Merge "Rename gr-settings-styles to gr-form-styles and update a few styles"
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 20d4e45..aa27f2b 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -497,7 +497,7 @@
 
 Deletion of references is also possible if `Push` with the force option
 is granted, however that includes the permission to fast-forward and
-force-update references to exiting and new commits. Being able to push
+force-update references to existing and new commits. Being able to push
 references for new commits is bad if bypassing of code review must be
 prevented.
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index b7c50f4..75d31d2c 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5689,6 +5689,14 @@
 |`destination`      ||Destination branch
 |`parent`           |optional, defaults to 1|
 Number of the parent relative to which the cherry-pick should be considered.
+|`notify`           |optional|
+Notify handling that defines to whom email notifications should be sent
+after the cherry-pick. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `NONE`.
+|`notify_details`   |optional|
+Additional information about whom to notify about the update as a map
+of recipient type to link:#notify-info[NotifyInfo] entity.
 |===========================
 
 [[comment-info]]
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 a4a2cbc..dd44cb9a6 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
@@ -38,12 +38,16 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.DraftApi;
 import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
@@ -70,6 +74,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Branch.NameKey;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.server.change.GetRevisionActions;
@@ -623,6 +628,47 @@
   }
 
   @Test
+  public void cherryPickNotify() throws Exception {
+    createBranch(new NameKey(project, "branch-1"));
+    createBranch(new NameKey(project, "branch-2"));
+    createBranch(new NameKey(project, "branch-3"));
+
+    // Creates a change for 'admin'.
+    PushOneCommit.Result result = createChange();
+    String changeId = project.get() + "~master~" + result.getChangeId();
+
+    // 'user' cherry-picks the change to a new branch, the source change's author/committer('admin')
+    // will be added as a reviewer of the newly created change.
+    setApiUser(user);
+    CherryPickInput input = new CherryPickInput();
+    input.message = "it goes to a new branch";
+
+    // Enable the notification. 'admin' as a reviewer should be notified.
+    input.destination = "branch-1";
+    input.notify = NotifyHandling.ALL;
+    sender.clear();
+    gApi.changes().id(changeId).current().cherryPick(input);
+    assertNotifyCc(admin);
+
+    // Disable the notification. 'admin' as a reviewer should not be notified any more.
+    input.destination = "branch-2";
+    input.notify = NotifyHandling.NONE;
+    sender.clear();
+    gApi.changes().id(changeId).current().cherryPick(input);
+    assertThat(sender.getMessages()).hasSize(0);
+
+    // Disable the notification. The user provided in the 'notifyDetails' should still be notified.
+    TestAccount userToNotify = accounts.user2();
+    input.destination = "branch-3";
+    input.notify = NotifyHandling.NONE;
+    input.notifyDetails =
+        ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email)));
+    sender.clear();
+    gApi.changes().id(changeId).current().cherryPick(input);
+    assertNotifyTo(userToNotify);
+  }
+
+  @Test
   public void canRebase() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
     PushOneCommit.Result r1 = push.to("refs/for/master");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 1a6166e..e4b5ff5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -33,6 +33,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
@@ -97,6 +98,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.RefSpec;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -802,6 +804,77 @@
     }
   }
 
+  @Test
+  public void submitWithCommitAndItsMergeCommitTogether() throws Exception {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+
+    RevCommit initialHead = getRemoteHead();
+
+    // Create a stable branch and bootstrap it.
+    gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
+    PushOneCommit push =
+        pushFactory.create(db, user.getIdent(), testRepo, "initial commit", "a.txt", "a");
+    PushOneCommit.Result change = push.to("refs/heads/stable");
+
+    RevCommit stable = getRemoteHead(project, "stable");
+    RevCommit master = getRemoteHead(project, "master");
+
+    assertThat(master).isEqualTo(initialHead);
+    assertThat(stable).isEqualTo(change.getCommit());
+
+    testRepo.git().fetch().call();
+    testRepo.git().branchCreate().setName("stable").setStartPoint(stable).call();
+    testRepo.git().branchCreate().setName("master").setStartPoint(master).call();
+
+    // Create a fix in stable branch.
+    testRepo.reset(stable);
+    RevCommit fix =
+        testRepo
+            .commit()
+            .parent(stable)
+            .message("small fix")
+            .add("b.txt", "b")
+            .insertChangeId()
+            .create();
+    testRepo.branch("refs/heads/stable").update(fix);
+    testRepo
+        .git()
+        .push()
+        .setRefSpecs(new RefSpec("refs/heads/stable:refs/for/stable/" + name("topic")))
+        .call();
+
+    // Merge the fix into master.
+    testRepo.reset(master);
+    RevCommit merge =
+        testRepo
+            .commit()
+            .parent(master)
+            .parent(fix)
+            .message("Merge stable into master")
+            .insertChangeId()
+            .create();
+    testRepo.branch("refs/heads/master").update(merge);
+    testRepo
+        .git()
+        .push()
+        .setRefSpecs(new RefSpec("refs/heads/master:refs/for/master/" + name("topic")))
+        .call();
+
+    // Submit together.
+    String fixId = GitUtil.getChangeId(testRepo, fix).get();
+    String mergeId = GitUtil.getChangeId(testRepo, merge).get();
+    approve(fixId);
+    approve(mergeId);
+    submit(mergeId);
+    assertMerged(fixId);
+    assertMerged(mergeId);
+    testRepo.git().fetch().call();
+    RevWalk rw = testRepo.getRevWalk();
+    master = rw.parseCommit(getRemoteHead(project, "master"));
+    assertThat(rw.isMergedInto(merge, master)).isTrue();
+    assertThat(rw.isMergedInto(fix, master)).isTrue();
+  }
+
   private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception {
     for (PushOneCommit.Result change : changes) {
       try (BatchUpdate bu =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index edf5420..fa1b95f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -18,11 +18,9 @@
 import static com.google.common.truth.TruthJUnit.assume;
 
 import com.google.common.collect.Iterables;
-import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
-import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -34,7 +32,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Test;
 
 public abstract class AbstractSubmitByMerge extends AbstractSubmit {
@@ -181,74 +178,4 @@
     }
   }
 
-  @Test
-  public void submitWithCommitAndItsMergeCommitTogether() throws Exception {
-    assume().that(isSubmitWholeTopicEnabled()).isTrue();
-
-    RevCommit initialHead = getRemoteHead();
-
-    // Create a stable branch and bootstrap it.
-    gApi.projects().name(project.get()).branch("stable").create(new BranchInput());
-    PushOneCommit push =
-        pushFactory.create(db, user.getIdent(), testRepo, "initial commit", "a.txt", "a");
-    PushOneCommit.Result change = push.to("refs/heads/stable");
-
-    RevCommit stable = getRemoteHead(project, "stable");
-    RevCommit master = getRemoteHead(project, "master");
-
-    assertThat(master).isEqualTo(initialHead);
-    assertThat(stable).isEqualTo(change.getCommit());
-
-    testRepo.git().fetch().call();
-    testRepo.git().branchCreate().setName("stable").setStartPoint(stable).call();
-    testRepo.git().branchCreate().setName("master").setStartPoint(master).call();
-
-    // Create a fix in stable branch.
-    testRepo.reset(stable);
-    RevCommit fix =
-        testRepo
-            .commit()
-            .parent(stable)
-            .message("small fix")
-            .add("b.txt", "b")
-            .insertChangeId()
-            .create();
-    testRepo.branch("refs/heads/stable").update(fix);
-    testRepo
-        .git()
-        .push()
-        .setRefSpecs(new RefSpec("refs/heads/stable:refs/for/stable/" + name("topic")))
-        .call();
-
-    // Merge the fix into master.
-    testRepo.reset(master);
-    RevCommit merge =
-        testRepo
-            .commit()
-            .parent(master)
-            .parent(fix)
-            .message("Merge stable into master")
-            .insertChangeId()
-            .create();
-    testRepo.branch("refs/heads/master").update(merge);
-    testRepo
-        .git()
-        .push()
-        .setRefSpecs(new RefSpec("refs/heads/master:refs/for/master/" + name("topic")))
-        .call();
-
-    // Submit together.
-    String fixId = GitUtil.getChangeId(testRepo, fix).get();
-    String mergeId = GitUtil.getChangeId(testRepo, merge).get();
-    approve(fixId);
-    approve(mergeId);
-    submit(mergeId);
-    assertMerged(fixId);
-    assertMerged(mergeId);
-    testRepo.git().fetch().call();
-    RevWalk rw = testRepo.getRevWalk();
-    master = rw.parseCommit(getRemoteHead(project, "master"));
-    assertThat(rw.isMergedInto(merge, master)).isTrue();
-    assertThat(rw.isMergedInto(fix, master)).isTrue();
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 16dbee3..383858d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -57,13 +57,16 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
+import java.util.Map.Entry;
 import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -777,79 +780,140 @@
 
   @Test
   public void deleteCommentByRewritingCommitHistory() throws Exception {
-    // Create change (the 1st commit on the change's meta branch).
-    PushOneCommit.Result result = createChange();
-    String changeId = result.getChangeId();
-    Change.Id id = result.getChange().getId();
+    // Creates the following commit history on the meta branch of the test change. Then tries to
+    // delete the comments one by one, which will rewrite most of the commits on the 'meta' branch.
+    // Commits will be rewritten N times for N added comments. After each deletion, the meta branch
+    // should keep its previous state except that the target comment's message should be updated.
 
-    // Add two comments in patch set 1 (the 2nd commit on the change's meta branch).
-    ReviewInput reviewInput = new ReviewInput();
-    CommentInput comment1 = newComment(FILE_NAME, Side.REVISION, 0, "My password: abc123", false);
-    CommentInput comment2 = newComment(FILE_NAME, Side.REVISION, 1, "nit: long line", false);
-    reviewInput.comments = ImmutableMap.of(FILE_NAME, Lists.newArrayList(comment1, comment2));
-    reviewInput.label("Code-Review", 1);
-    gApi.changes().id(changeId).current().review(reviewInput);
+    // 1st commit: Create PS1.
+    PushOneCommit.Result result1 = createChange(SUBJECT, "a.txt", "a");
+    Change.Id id = result1.getChange().getId();
+    String changeId = result1.getChangeId();
+    String ps1 = result1.getCommit().name();
 
-    // Create patch set 2 (the 3rd commit on the change's meta branch).
-    amendChange(changeId);
+    // 2nd commit: Add (c1) to PS1.
+    CommentInput c1 = newComment("a.txt", "comment 1");
+    addComments(changeId, ps1, c1);
 
-    // Add 'comment3' in patch set 2 (the 4th commit on the change's meta branch).
-    CommentInput comment3 = addComment(changeId, "typo");
+    // 3rd commit: Add (c2, c3) to PS1.
+    CommentInput c2 = newComment("a.txt", "comment 2");
+    CommentInput c3 = newComment("a.txt", "comment 3");
+    addComments(changeId, ps1, c2, c3);
 
-    Map<String, List<CommentInfo>> commentsMap = getPublishedComments(changeId);
-    assertThat(commentsMap).hasSize(1);
-    assertThat(commentsMap.get(FILE_NAME)).hasSize(3);
-    Optional<CommentInfo> targetCommentInfo =
-        commentsMap
-            .get(FILE_NAME)
-            .stream()
-            .filter(c -> c.message.equals("My password: abc123"))
-            .findFirst();
-    assertThat(targetCommentInfo.isPresent()).isTrue();
+    // 4th commit: Add (c4) to PS1.
+    CommentInput c4 = newComment("a.txt", "comment 4");
+    addComments(changeId, ps1, c4);
 
-    List<RevCommit> commitsBeforeDelete = new ArrayList<>();
-    if (notesMigration.commitChangeWrites()) {
-      commitsBeforeDelete = getCommits(id);
-    }
+    // 5th commit: Create PS2.
+    PushOneCommit.Result result2 = amendChange(changeId, "refs/for/master", "b.txt", "b");
+    String ps2 = result2.getCommit().name();
 
-    String uuid = targetCommentInfo.get().id;
-    // Get the target comment.
-    CommentInfo oldComment =
-        gApi.changes().id(changeId).revision(result.getCommit().getName()).comment(uuid).get();
+    // 6th commit: Add (c5) to PS1.
+    CommentInput c5 = newComment("a.txt", "comment 5");
+    addComments(changeId, ps1, c5);
 
-    // Delete the target comment.
-    DeleteCommentInput input = new DeleteCommentInput("contains confidential information");
+    // 7th commit: Add (c6) to PS2.
+    CommentInput c6 = newComment("b.txt", "comment 6");
+    addComments(changeId, ps2, c6);
+
+    // 8th commit: Create PS3.
+    PushOneCommit.Result result3 = amendChange(changeId);
+    String ps3 = result3.getCommit().name();
+
+    // 9th commit: Create PS4.
+    PushOneCommit.Result result4 = amendChange(changeId, "refs/for/master", "c.txt", "c");
+    String ps4 = result4.getCommit().name();
+
+    // 10th commit: Add (c7, c8) to PS4.
+    CommentInput c7 = newComment("c.txt", "comment 7");
+    CommentInput c8 = newComment("b.txt", "comment 8");
+    addComments(changeId, ps4, c7, c8);
+
+    // 11th commit: Add (c9) to PS2.
+    CommentInput c9 = newComment("b.txt", "comment 9");
+    addComments(changeId, ps2, c9);
+
+    List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(changeId);
+    assertThat(commentsBeforeDelete).hasSize(9);
+    // PS1 has comments [c1, c2, c3, c4, c5].
+    assertThat(getRevisionComments(changeId, ps1)).hasSize(5);
+    // PS2 has comments [c6, c9].
+    assertThat(getRevisionComments(changeId, ps2)).hasSize(2);
+    // PS3 has no comment.
+    assertThat(getRevisionComments(changeId, ps3)).hasSize(0);
+    // PS4 has comments [c7, c8].
+    assertThat(getRevisionComments(changeId, ps4)).hasSize(2);
+
     setApiUser(admin);
-    CommentInfo updatedComment =
-        gApi.changes()
-            .id(changeId)
-            .revision(result.getCommit().getName())
-            .comment(uuid)
-            .delete(input);
+    for (int i = 0; i < commentsBeforeDelete.size(); i++) {
+      List<RevCommit> commitsBeforeDelete = new ArrayList<>();
+      if (notesMigration.commitChangeWrites()) {
+        commitsBeforeDelete = getCommits(id);
+      }
 
-    String expectedMsg =
-        String.format("Comment removed by: %s; Reason: %s", admin.fullName, input.reason);
+      CommentInfo comment = commentsBeforeDelete.get(i);
+      String uuid = comment.id;
+      int patchSet = comment.patchSet;
+      // 'oldComment' has some fields unset compared with 'comment'.
+      CommentInfo oldComment = gApi.changes().id(changeId).revision(patchSet).comment(uuid).get();
 
-    assertThat(updatedComment.message).isEqualTo(expectedMsg);
-    updatedComment.message = oldComment.message;
-    assertThat(updatedComment).isEqualTo(oldComment);
+      DeleteCommentInput input = new DeleteCommentInput("delete comment " + uuid);
+      CommentInfo updatedComment =
+          gApi.changes().id(changeId).revision(patchSet).comment(uuid).delete(input);
 
-    // Check the comment's message has been replaced in NoteDb.
-    if (notesMigration.commitChangeWrites()) {
-      assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
+      String expectedMsg =
+          String.format("Comment removed by: %s; Reason: %s", admin.fullName, input.reason);
+      assertThat(updatedComment.message).isEqualTo(expectedMsg);
+      oldComment.message = expectedMsg;
+      assertThat(updatedComment).isEqualTo(oldComment);
+
+      // Check the NoteDb state after the deletion.
+      if (notesMigration.commitChangeWrites()) {
+        assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
+      }
+
+      comment.message = expectedMsg;
+      commentsBeforeDelete.set(i, comment);
+      List<CommentInfo> commentsAfterDelete = getChangeSortedComments(changeId);
+      assertThat(commentsAfterDelete).isEqualTo(commentsBeforeDelete);
     }
 
     // Make sure that comments can still be added correctly.
-    CommentInput comment4 = addComment(changeId, "too much space");
-    commentsMap = getPublishedComments(changeId);
+    CommentInput c10 = newComment("a.txt", "comment 10");
+    CommentInput c11 = newComment("b.txt", "comment 11");
+    CommentInput c12 = newComment("a.txt", "comment 12");
+    CommentInput c13 = newComment("c.txt", "comment 13");
+    addComments(changeId, ps1, c10);
+    addComments(changeId, ps2, c11);
+    addComments(changeId, ps3, c12);
+    addComments(changeId, ps4, c13);
 
-    assertThat(commentsMap).hasSize(1);
-    List<CommentInput> comments =
-        Lists.transform(commentsMap.get(FILE_NAME), infoToInput(FILE_NAME));
+    assertThat(getChangeSortedComments(changeId)).hasSize(13);
+    assertThat(getRevisionComments(changeId, ps1)).hasSize(6);
+    assertThat(getRevisionComments(changeId, ps2)).hasSize(3);
+    assertThat(getRevisionComments(changeId, ps3)).hasSize(1);
+    assertThat(getRevisionComments(changeId, ps4)).hasSize(3);
+  }
 
-    // Change comment1's message to the expected message.
-    comment1.message = expectedMsg;
-    assertThat(comments).containsExactly(comment1, comment2, comment3, comment4);
+  private List<CommentInfo> getChangeSortedComments(String changeId) throws Exception {
+    List<CommentInfo> comments = new ArrayList<>();
+    Map<String, List<CommentInfo>> commentsMap = getPublishedComments(changeId);
+    for (Entry<String, List<CommentInfo>> e : commentsMap.entrySet()) {
+      for (CommentInfo c : e.getValue()) {
+        c.path = e.getKey(); // Set the comment's path field.
+        comments.add(c);
+      }
+    }
+    comments.sort(Comparator.comparing(c -> c.id));
+    return comments;
+  }
+
+  private List<CommentInfo> getRevisionComments(String changeId, String revId) throws Exception {
+    return getPublishedComments(changeId, revId)
+        .values()
+        .stream()
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
   }
 
   private CommentInput addComment(String changeId, String message) throws Exception {
@@ -860,6 +924,13 @@
     return comment;
   }
 
+  private void addComments(String changeId, String revision, CommentInput... commentInputs)
+      throws Exception {
+    ReviewInput input = new ReviewInput();
+    input.comments = Arrays.stream(commentInputs).collect(Collectors.groupingBy(c -> c.path));
+    gApi.changes().id(changeId).revision(revision).review(input);
+  }
+
   private List<RevCommit> getCommits(Change.Id changeId) throws IOException {
     try (Repository repo = repoManager.openRepository(project);
         RevWalk revWalk = new RevWalk(repo)) {
@@ -989,6 +1060,10 @@
     return gApi.changes().id(changeId).revision(revId).draft(uuid).get();
   }
 
+  private static CommentInput newComment(String file, String message) {
+    return newComment(file, Side.REVISION, 0, message, false);
+  }
+
   private static CommentInput newComment(
       String path, Side side, int line, String message, Boolean unresolved) {
     CommentInput c = new CommentInput();
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 2e1bb13..3ac3601 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
@@ -14,8 +14,13 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import java.util.Map;
+
 public class CherryPickInput {
   public String message;
   public String destination;
   public Integer parent;
+
+  public NotifyHandling notify = NotifyHandling.NONE;
+  public Map<RecipientType, NotifyInfo> notifyDetails;
 }
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 1a1f8cc..35aa4ad 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
@@ -66,7 +66,7 @@
       BatchUpdate.Factory updateFactory, RevisionResource revision, CherryPickInput input)
       throws OrmException, IOException, UpdateException, RestApiException {
     final ChangeControl control = revision.getControl();
-    int parent = input.parent == null ? 1 : input.parent;
+    input.parent = input.parent == null ? 1 : input.parent;
 
     if (input.message == null || input.message.trim().isEmpty()) {
       throw new BadRequestException("message must be non-empty");
@@ -100,10 +100,9 @@
               updateFactory,
               revision.getChange(),
               revision.getPatchSet(),
-              input.message,
+              input,
               refName,
-              refControl,
-              parent);
+              refControl);
       return json.noOptions().format(revision.getProject(), cherryPickedChangeId);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
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 a540298..7c0a7be 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
@@ -18,7 +18,8 @@
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -80,6 +81,7 @@
   private final MergeUtil.Factory mergeUtilFactory;
   private final ChangeMessagesUtil changeMessagesUtil;
   private final PatchSetUtil psUtil;
+  private final NotifyUtil notifyUtil;
 
   @Inject
   CherryPickChange(
@@ -93,7 +95,8 @@
       PatchSetInserter.Factory patchSetInserterFactory,
       MergeUtil.Factory mergeUtilFactory,
       ChangeMessagesUtil changeMessagesUtil,
-      PatchSetUtil psUtil) {
+      PatchSetUtil psUtil,
+      NotifyUtil notifyUtil) {
     this.db = db;
     this.seq = seq;
     this.queryProvider = queryProvider;
@@ -105,16 +108,16 @@
     this.mergeUtilFactory = mergeUtilFactory;
     this.changeMessagesUtil = changeMessagesUtil;
     this.psUtil = psUtil;
+    this.notifyUtil = notifyUtil;
   }
 
   public Change.Id cherryPick(
       BatchUpdate.Factory batchUpdateFactory,
       Change change,
       PatchSet patch,
-      String message,
+      CherryPickInput input,
       String ref,
-      RefControl refControl,
-      int parent)
+      RefControl refControl)
       throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
           UpdateException, RestApiException {
     return cherryPick(
@@ -125,10 +128,9 @@
         change.getTopic(),
         change.getProject(),
         ObjectId.fromString(patch.getRevision().get()),
-        message,
+        input,
         ref,
-        refControl,
-        parent);
+        refControl);
   }
 
   public Change.Id cherryPick(
@@ -139,10 +141,9 @@
       @Nullable String sourceChangeTopic,
       Project.NameKey project,
       ObjectId sourceCommit,
-      String message,
+      CherryPickInput input,
       String targetRef,
-      RefControl targetRefControl,
-      int parent)
+      RefControl targetRefControl)
       throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
           UpdateException, RestApiException {
 
@@ -170,12 +171,12 @@
 
       CodeReviewCommit commitToCherryPick = revWalk.parseCommit(sourceCommit);
 
-      if (parent <= 0 || parent > commitToCherryPick.getParentCount()) {
+      if (input.parent <= 0 || input.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()));
+                input.parent, commitToCherryPick.getParentCount()));
       }
 
       Timestamp now = TimeUtil.nowTs();
@@ -187,8 +188,8 @@
               mergeTip,
               commitToCherryPick.getAuthorIdent(),
               committerIdent,
-              message);
-      String commitMessage = ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n';
+              input.message);
+      String commitMessage = ChangeIdUtil.insertId(input.message, computedChangeId).trim() + '\n';
 
       CodeReviewCommit cherryPickCommit;
       try {
@@ -204,7 +205,7 @@
                     committerIdent,
                     commitMessage,
                     revWalk,
-                    parent - 1,
+                    input.parent - 1,
                     false);
 
         Change.Key changeKey;
@@ -234,7 +235,7 @@
             // will be added as a new patch set.
             ChangeControl destCtl =
                 targetRefControl.getProjectControl().controlFor(destChanges.get(0).notes());
-            result = insertPatchSet(bu, git, destCtl, cherryPickCommit);
+            result = insertPatchSet(bu, git, destCtl, cherryPickCommit, input);
           } else {
             // Change key not found on destination branch. We can create a new
             // change.
@@ -249,7 +250,8 @@
                     targetRefControl.getRefName(),
                     newTopic,
                     sourceBranch,
-                    sourceCommit);
+                    sourceCommit,
+                    input);
 
             if (sourceChangeId != null && sourcePatchId != null) {
               bu.addOp(
@@ -268,20 +270,23 @@
   }
 
   private Change.Id insertPatchSet(
-      BatchUpdate bu, Repository git, ChangeControl destCtl, CodeReviewCommit cherryPickCommit)
-      throws IOException, OrmException {
+      BatchUpdate bu,
+      Repository git,
+      ChangeControl destCtl,
+      CodeReviewCommit cherryPickCommit,
+      CherryPickInput input)
+      throws IOException, OrmException, BadRequestException {
     Change destChange = destCtl.getChange();
     PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
-    PatchSetInserter inserter = patchSetInserterFactory.create(destCtl, psId, cherryPickCommit);
-    PatchSet.Id newPatchSetId = inserter.getPatchSetId();
     PatchSet current = psUtil.current(db.get(), destCtl.getNotes());
 
-    bu.addOp(
-        destChange.getId(),
-        inserter
-            .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
-            .setDraft(current.isDraft())
-            .setNotify(NotifyHandling.NONE));
+    PatchSetInserter inserter = patchSetInserterFactory.create(destCtl, psId, cherryPickCommit);
+    inserter
+        .setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".")
+        .setDraft(current.isDraft())
+        .setNotify(input.notify)
+        .setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+    bu.addOp(destChange.getId(), inserter);
     return destChange.getId();
   }
 
@@ -291,12 +296,15 @@
       String refName,
       String topic,
       Branch.NameKey sourceBranch,
-      ObjectId sourceCommit)
-      throws OrmException, IOException {
+      ObjectId sourceCommit,
+      CherryPickInput input)
+      throws OrmException, IOException, BadRequestException {
     Change.Id changeId = new Change.Id(seq.nextChangeId());
     ChangeInserter ins =
         changeInserterFactory.create(changeId, cherryPickCommit, refName).setTopic(topic);
-    ins.setMessage(messageForDestinationChange(ins.getPatchSetId(), sourceBranch, sourceCommit));
+    ins.setMessage(messageForDestinationChange(ins.getPatchSetId(), sourceBranch, sourceCommit))
+        .setNotify(input.notify)
+        .setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
     bu.insertChange(ins);
     return changeId;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
index 4c027dd..b44a8b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
@@ -59,9 +59,11 @@
   public ChangeInfo applyImpl(
       BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input)
       throws OrmException, IOException, UpdateException, RestApiException {
+    RevCommit commit = rsrc.getCommit();
     String message = Strings.nullToEmpty(input.message).trim();
+    input.message = message.isEmpty() ? commit.getFullMessage() : message;
     String destination = Strings.nullToEmpty(input.destination).trim();
-    int parent = input.parent == null ? 1 : input.parent;
+    input.parent = input.parent == null ? 1 : input.parent;
 
     if (destination.isEmpty()) {
       throw new BadRequestException("destination must be non-empty");
@@ -73,7 +75,6 @@
       throw new AuthException(capable.getMessage());
     }
 
-    RevCommit commit = rsrc.getCommit();
     String refName = RefNames.fullName(destination);
     RefControl refControl = projectControl.controlForRef(refName);
     if (!refControl.canUpload()) {
@@ -84,17 +85,7 @@
     try {
       Change.Id cherryPickedChangeId =
           cherryPickChange.cherryPick(
-              updateFactory,
-              null,
-              null,
-              null,
-              null,
-              project,
-              commit,
-              message.isEmpty() ? commit.getFullMessage() : message,
-              refName,
-              refControl,
-              parent);
+              updateFactory, null, null, null, null, project, commit, input, refName, refControl);
       return json.noOptions().format(project, cherryPickedChangeId);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
index 78fc495..733bf49 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
@@ -29,20 +29,20 @@
   private final CodeReviewRevWalk rw;
   private final RevFlag canMergeFlag;
   private final Set<RevCommit> accepted;
+  private final Set<CodeReviewCommit> incoming;
 
-  public MergeSorter(CodeReviewRevWalk rw, Set<RevCommit> alreadyAccepted, RevFlag canMergeFlag) {
+  public MergeSorter(
+      CodeReviewRevWalk rw,
+      Set<RevCommit> alreadyAccepted,
+      RevFlag canMergeFlag,
+      Set<CodeReviewCommit> incoming) {
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
     this.accepted = alreadyAccepted;
+    this.incoming = incoming;
   }
 
   Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> toMerge) throws IOException {
-    return sort(toMerge, toMerge);
-  }
-
-  Collection<CodeReviewCommit> sort(
-      final Collection<CodeReviewCommit> toMerge, final Collection<CodeReviewCommit> incoming)
-      throws IOException {
     final Set<CodeReviewCommit> heads = new HashSet<>();
     final Set<CodeReviewCommit> sort = new HashSet<>(toMerge);
     while (!sort.isEmpty()) {
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 6446fdd..11e3051 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
@@ -209,11 +209,10 @@
   }
 
   public List<CodeReviewCommit> reduceToMinimalMerge(
-      MergeSorter mergeSorter, Collection<CodeReviewCommit> toSort, Set<CodeReviewCommit> incoming)
-      throws IntegrationException {
+      MergeSorter mergeSorter, Collection<CodeReviewCommit> toSort) throws IntegrationException {
     List<CodeReviewCommit> result = new ArrayList<>();
     try {
-      result.addAll(mergeSorter.sort(toSort, incoming));
+      result.addAll(mergeSorter.sort(toSort));
     } catch (IOException e) {
       throw new IntegrationException("Branch head sorting failed", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
index 94e78f8..dbfb19c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
@@ -42,23 +42,26 @@
   private final RevCommit initialTip;
   private final Set<RevCommit> alreadyAccepted;
   private final InternalChangeQuery internalChangeQuery;
+  private final Set<CodeReviewCommit> incoming;
 
   public RebaseSorter(
       CodeReviewRevWalk rw,
       RevCommit initialTip,
       Set<RevCommit> alreadyAccepted,
       RevFlag canMergeFlag,
-      InternalChangeQuery internalChangeQuery) {
+      InternalChangeQuery internalChangeQuery,
+      Set<CodeReviewCommit> incoming) {
     this.rw = rw;
     this.canMergeFlag = canMergeFlag;
     this.initialTip = initialTip;
     this.alreadyAccepted = alreadyAccepted;
     this.internalChangeQuery = internalChangeQuery;
+    this.incoming = incoming;
   }
 
-  public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> incoming) throws IOException {
+  public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort) throws IOException {
     final List<CodeReviewCommit> sorted = new ArrayList<>();
-    final Set<CodeReviewCommit> sort = new HashSet<>(incoming);
+    final Set<CodeReviewCommit> sort = new HashSet<>(toSort);
     while (!sort.isEmpty()) {
       final CodeReviewCommit n = removeOne(sort);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
index 7151486..38a193d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
@@ -29,8 +29,7 @@
   @Override
   public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge)
       throws IntegrationException {
-    List<CodeReviewCommit> sorted =
-        args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge, args.incoming);
+    List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
     CodeReviewCommit newTipCommit =
         args.mergeUtil.getFirstFastForward(args.mergeTip.getInitialTip(), args.rw, sorted);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
index ce045f8..1664be4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
@@ -28,8 +28,7 @@
   @Override
   public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge)
       throws IntegrationException {
-    List<CodeReviewCommit> sorted =
-        args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge, args.incoming);
+    List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
     if (args.mergeTip.getInitialTip() == null && !sorted.isEmpty()) {
       // The branch is unborn. Take a fast-forward resolution to
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
index e7db1a8..d30aab2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -28,8 +28,7 @@
   @Override
   public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge)
       throws IntegrationException {
-    List<CodeReviewCommit> sorted =
-        args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge, args.incoming);
+    List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
 
     if (args.mergeTip.getInitialTip() == null
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
index 40bb6c1..d4487b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeIdenticalTreeException;
 import com.google.gerrit.server.git.MergeTip;
-import com.google.gerrit.server.git.RebaseSorter;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -57,7 +56,12 @@
   @Override
   public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge)
       throws IntegrationException {
-    List<CodeReviewCommit> sorted = sort(toMerge);
+    List<CodeReviewCommit> sorted;
+    try {
+      sorted = args.rebaseSorter.sort(toMerge);
+    } catch (IOException e) {
+      throw new IntegrationException("Commit sorting failed", e);
+    }
     List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
     boolean first = true;
 
@@ -67,7 +71,7 @@
         // MERGE_IF_NECESSARY semantics to avoid creating duplicate
         // commits.
         //
-        sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted, args.incoming);
+        sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
         break;
       }
     }
@@ -286,21 +290,6 @@
     args.alreadyAccepted.add(mergeTip.getCurrentTip());
   }
 
-  private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort)
-      throws IntegrationException {
-    try {
-      return new RebaseSorter(
-              args.rw,
-              args.mergeTip.getInitialTip(),
-              args.alreadyAccepted,
-              args.canMergeFlag,
-              args.internalChangeQuery)
-          .sort(toSort);
-    } catch (IOException e) {
-      throw new IntegrationException("Commit sorting failed", e);
-    }
-  }
-
   static boolean dryRun(
       SubmitDryRun.Arguments args,
       Repository repo,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
index da35f78..0d012e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git.strategy;
 
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.server.git.CodeReviewCommit;
@@ -108,7 +109,7 @@
             repo,
             rw,
             mergeUtilFactory.create(getProject(destBranch)),
-            new MergeSorter(rw, alreadyAccepted, canMerge));
+            new MergeSorter(rw, alreadyAccepted, canMerge, ImmutableSet.of(toMergeCommit)));
 
     switch (submitType) {
       case CHERRY_PICK:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index e9ed294..95bdf33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.server.git.MergeSorter;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.RebaseSorter;
 import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.validators.OnSubmitValidators;
@@ -126,7 +127,6 @@
     final RevFlag canMergeFlag;
     final ReviewDb db;
     final Set<RevCommit> alreadyAccepted;
-    final Set<CodeReviewCommit> incoming;
     final RequestId submissionId;
     final SubmitType submitType;
     final NotifyHandling notifyHandling;
@@ -135,6 +135,7 @@
 
     final ProjectState project;
     final MergeSorter mergeSorter;
+    final RebaseSorter rebaseSorter;
     final MergeUtil mergeUtil;
     final boolean dryrun;
 
@@ -196,7 +197,6 @@
       this.canMergeFlag = canMergeFlag;
       this.db = db;
       this.alreadyAccepted = alreadyAccepted;
-      this.incoming = incoming;
       this.submissionId = submissionId;
       this.submitType = submitType;
       this.notifyHandling = notifyHandling;
@@ -209,7 +209,15 @@
               projectCache.get(destBranch.getParentKey()),
               "project not found: %s",
               destBranch.getParentKey());
-      this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
+      this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag, incoming);
+      this.rebaseSorter =
+          new RebaseSorter(
+              rw,
+              mergeTip.getInitialTip(),
+              alreadyAccepted,
+              canMergeFlag,
+              internalChangeQuery,
+              incoming);
       this.mergeUtil = mergeUtilFactory.create(project);
       this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java
index 33cca1e..733fcce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java
@@ -64,7 +64,7 @@
     reindexers = Maps.newHashMapWithExpectedSize(defs.size());
     onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
     runReindexMsg =
-        "No index versions ready; run java -jar "
+        "No index versions for index '%s' ready; run java -jar "
             + sitePaths.gerrit_war.toAbsolutePath()
             + " reindex";
   }
@@ -142,7 +142,7 @@
       }
     }
     if (search == null) {
-      throw new ProvisionException(runReindexMsg);
+      throw new ProvisionException(String.format(runReindexMsg, def.getName()));
     }
 
     IndexFactory<K, V, I> factory = def.getIndexFactory();
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 8765607..a3bd230 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
@@ -74,10 +74,12 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Stream;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.FooterLine;
 
 /**
@@ -324,10 +326,36 @@
     return SchemaUtil.getPersonParts(cd.getAuthor());
   }
 
+  public static Set<String> getAuthorNameAndEmail(ChangeData cd) throws OrmException, IOException {
+    return getNameAndEmail(cd.getAuthor());
+  }
+
   public static Set<String> getCommitterParts(ChangeData cd) throws OrmException, IOException {
     return SchemaUtil.getPersonParts(cd.getCommitter());
   }
 
+  public static Set<String> getCommitterNameAndEmail(ChangeData cd)
+      throws OrmException, IOException {
+    return getNameAndEmail(cd.getCommitter());
+  }
+
+  private static Set<String> getNameAndEmail(PersonIdent person) {
+    if (person == null) {
+      return ImmutableSet.of();
+    }
+
+    String name = person.getName().toLowerCase(Locale.US);
+    String email = person.getEmailAddress().toLowerCase(Locale.US);
+
+    StringBuilder nameEmailBuilder = new StringBuilder();
+    PersonIdent.appendSanitized(nameEmailBuilder, name);
+    nameEmailBuilder.append(" <");
+    PersonIdent.appendSanitized(nameEmailBuilder, email);
+    nameEmailBuilder.append('>');
+
+    return ImmutableSet.of(name, email, nameEmailBuilder.toString());
+  }
+
   /**
    * The exact email address, or any part of the author name or email address, in the current patch
    * set.
@@ -335,6 +363,11 @@
   public static final FieldDef<ChangeData, Iterable<String>> AUTHOR =
       fullText(ChangeQueryBuilder.FIELD_AUTHOR).buildRepeatable(ChangeField::getAuthorParts);
 
+  /** The exact name, email address and NameEmail of the author. */
+  public static final FieldDef<ChangeData, Iterable<String>> EXACT_AUTHOR =
+      exact(ChangeQueryBuilder.FIELD_EXACTAUTHOR)
+          .buildRepeatable(ChangeField::getAuthorNameAndEmail);
+
   /**
    * The exact email address, or any part of the committer name or email address, in the current
    * patch set.
@@ -342,6 +375,11 @@
   public static final FieldDef<ChangeData, Iterable<String>> COMMITTER =
       fullText(ChangeQueryBuilder.FIELD_COMMITTER).buildRepeatable(ChangeField::getCommitterParts);
 
+  /** The exact name, email address, and NameEmail of the committer. */
+  public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMITTER =
+      exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER)
+          .buildRepeatable(ChangeField::getCommitterNameAndEmail);
+
   public static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class);
 
   /** Serialized change object, used for pre-populating results. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index ec507f4..be4f24b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -72,8 +72,10 @@
 
   @Deprecated static final Schema<ChangeData> V40 = schema(V39, ChangeField.PRIVATE);
   @Deprecated static final Schema<ChangeData> V41 = schema(V40, ChangeField.REVIEWER_BY_EMAIL);
+  @Deprecated static final Schema<ChangeData> V42 = schema(V41, ChangeField.WIP);
 
-  static final Schema<ChangeData> V42 = schema(V41, ChangeField.WIP);
+  static final Schema<ChangeData> V43 =
+      schema(V42, ChangeField.EXACT_AUTHOR, ChangeField.EXACT_COMMITTER);
 
   public static final String NAME = "changes";
   public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
index 8a43bc6..c11e6c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -95,9 +95,9 @@
 
     ObjectReader reader = revWalk.getObjectReader();
     ObjectId newTip = revWalk.next(); // The first commit will not be rewritten.
-    NoteMap newTipNoteMap = NoteMap.read(reader, revWalk.parseCommit(newTip));
     Map<String, Comment> parentComments =
-        getPublishedComments(noteUtil, changeId, reader, newTipNoteMap);
+        getPublishedComments(
+            noteUtil, changeId, reader, NoteMap.read(reader, revWalk.parseCommit(newTip)));
 
     boolean rewrite = false;
     RevCommit originalCommit;
@@ -120,13 +120,12 @@
       newTip =
           rewriteCommit(
               originalCommit,
-              newTipNoteMap,
+              NoteMap.read(reader, revWalk.parseCommit(newTip)),
               newTip,
               inserter,
               reader,
               putInComments,
               deletedComments);
-      newTipNoteMap = NoteMap.read(reader, revWalk.parseCommit(newTip));
       parentComments = currComments;
     }
 
@@ -229,8 +228,9 @@
       byte[] data = entry.getValue().build(noteUtil, noteUtil.getWriteJson());
       if (data.length == 0) {
         revNotesMap.noteMap.remove(objectId);
+      } else {
+        revNotesMap.noteMap.set(objectId, inserter.insert(OBJ_BLOB, data));
       }
-      revNotesMap.noteMap.set(objectId, inserter.insert(OBJ_BLOB, data));
     }
 
     CommitBuilder cb = new CommitBuilder();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
index aec8442..deec7e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
@@ -61,7 +61,7 @@
     MutableInteger p = new MutableInteger();
     trimLeadingEmptyLines(raw, p);
     if (p.value >= raw.length) {
-      comments = null;
+      comments = ImmutableList.of();
       return;
     }
 
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 39e9241..af0a3b8 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
@@ -57,6 +57,7 @@
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.SchemaUtil;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -87,7 +88,9 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
@@ -127,6 +130,7 @@
   public static final String FIELD_AGE = "age";
   public static final String FIELD_ASSIGNEE = "assignee";
   public static final String FIELD_AUTHOR = "author";
+  public static final String FIELD_EXACTAUTHOR = "exactauthor";
   public static final String FIELD_BEFORE = "before";
   public static final String FIELD_CHANGE = "change";
   public static final String FIELD_CHANGE_ID = "change_id";
@@ -134,6 +138,7 @@
   public static final String FIELD_COMMENTBY = "commentby";
   public static final String FIELD_COMMIT = "commit";
   public static final String FIELD_COMMITTER = "committer";
+  public static final String FIELD_EXACTCOMMITTER = "exactcommitter";
   public static final String FIELD_CONFLICTS = "conflicts";
   public static final String FIELD_DELETED = "deleted";
   public static final String FIELD_DELTA = "delta";
@@ -1119,13 +1124,21 @@
   }
 
   @Operator
-  public Predicate<ChangeData> author(String who) {
-    return new AuthorPredicate(who);
+  public Predicate<ChangeData> author(String who) throws QueryParseException {
+    if (args.getSchema().hasField(ChangeField.EXACT_AUTHOR)) {
+      return getAuthorOrCommitterPredicate(
+          who.trim(), ExactAuthorPredicate::new, AuthorPredicate::new);
+    }
+    return getAuthorOrCommitterFullTextPredicate(who.trim(), AuthorPredicate::new);
   }
 
   @Operator
-  public Predicate<ChangeData> committer(String who) {
-    return new CommitterPredicate(who);
+  public Predicate<ChangeData> committer(String who) throws QueryParseException {
+    if (args.getSchema().hasField(ChangeField.EXACT_COMMITTER)) {
+      return getAuthorOrCommitterPredicate(
+          who.trim(), ExactCommitterPredicate::new, CommitterPredicate::new);
+    }
+    return getAuthorOrCommitterFullTextPredicate(who.trim(), CommitterPredicate::new);
   }
 
   @Operator
@@ -1199,6 +1212,30 @@
     return Predicate.or(predicates);
   }
 
+  private Predicate<ChangeData> getAuthorOrCommitterPredicate(
+      String who,
+      Function<String, Predicate<ChangeData>> exactPredicateFunc,
+      Function<String, Predicate<ChangeData>> fullPredicateFunc)
+      throws QueryParseException {
+    if (Address.tryParse(who) != null) {
+      return exactPredicateFunc.apply(who);
+    }
+    return getAuthorOrCommitterFullTextPredicate(who, fullPredicateFunc);
+  }
+
+  private Predicate<ChangeData> getAuthorOrCommitterFullTextPredicate(
+      String who, Function<String, Predicate<ChangeData>> fullPredicateFunc)
+      throws QueryParseException {
+    Set<String> parts = SchemaUtil.getNameParts(who);
+    if (parts.isEmpty()) {
+      throw error("invalid value");
+    }
+
+    List<Predicate<ChangeData>> predicates =
+        parts.stream().map(fullPredicateFunc).collect(Collectors.toList());
+    return Predicate.and(predicates);
+  }
+
   private Set<Account.Id> parseAccount(String who) throws QueryParseException, OrmException {
     if (isSelf(who)) {
       return Collections.singleton(self());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
new file mode 100644
index 0000000..bca5d3b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static com.google.gerrit.server.index.change.ChangeField.EXACT_AUTHOR;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTAUTHOR;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.Locale;
+
+public class ExactAuthorPredicate extends ChangeIndexPredicate {
+  public ExactAuthorPredicate(String value) {
+    super(EXACT_AUTHOR, FIELD_EXACTAUTHOR, value.toLowerCase(Locale.US));
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    try {
+      return ChangeField.getAuthorNameAndEmail(object).contains(getValue());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
new file mode 100644
index 0000000..3fae5e5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static com.google.gerrit.server.index.change.ChangeField.EXACT_COMMITTER;
+import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTCOMMITTER;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import java.util.Locale;
+
+public class ExactCommitterPredicate extends ChangeIndexPredicate {
+  public ExactCommitterPredicate(String value) {
+    super(EXACT_COMMITTER, FIELD_EXACTCOMMITTER, value.toLowerCase(Locale.US));
+  }
+
+  @Override
+  public boolean match(ChangeData object) throws OrmException {
+    try {
+      return ChangeField.getCommitterNameAndEmail(object).contains(getValue());
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 34536ab..ba37a5e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -124,6 +124,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -493,57 +494,82 @@
   }
 
   @Test
-  public void byAuthor() throws Exception {
-    TestRepository<Repo> repo = createProject("repo");
-    Change change1 = insert(repo, newChange(repo), userId);
-
-    // By exact email address
-    assertQuery("author:jauthor@example.com", change1);
-
-    // By email address part
-    assertQuery("author:jauthor", change1);
-    assertQuery("author:example", change1);
-    assertQuery("author:example.com", change1);
-
-    // By name part
-    assertQuery("author:Author", change1);
-
-    // Case insensitive
-    assertQuery("author:jAuThOr", change1);
-    assertQuery("author:ExAmPlE", change1);
-
-    // By non-existing email address / name / part
-    assertQuery("author:jcommitter@example.com");
-    assertQuery("author:somewhere.com");
-    assertQuery("author:jcommitter");
-    assertQuery("author:Committer");
+  public void byAuthorExact() throws Exception {
+    byAuthorOrCommitterExact("author:");
   }
 
   @Test
-  public void byCommitter() throws Exception {
+  public void byAuthorFullText() throws Exception {
+    byAuthorOrCommitterFullText("author:");
+  }
+
+  @Test
+  public void byCommitterExact() throws Exception {
+    byAuthorOrCommitterExact("committer:");
+  }
+
+  @Test
+  public void byCommitterFullText() throws Exception {
+    byAuthorOrCommitterFullText("committer:");
+  }
+
+  private void byAuthorOrCommitterExact(String searchOperator) throws Exception {
     TestRepository<Repo> repo = createProject("repo");
-    Change change1 = insert(repo, newChange(repo), userId);
+    PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
+    PersonIdent john = new PersonIdent("John", "john@example.com");
+    PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
+    Change change1 = createChange(repo, johnDoe);
+    Change change2 = createChange(repo, john);
+    Change change3 = createChange(repo, doeSmith);
 
-    // By exact email address
-    assertQuery("committer:jcommitter@example.com", change1);
+    // Only email address.
+    assertQuery(searchOperator + "john.doe@example.com", change1);
+    assertQuery(searchOperator + "john@example.com", change2);
+    assertQuery(searchOperator + "Doe_SmIth@example.com", change3); // Case insensitive.
 
-    // By email address part
-    assertQuery("committer:jcommitter", change1);
-    assertQuery("committer:example", change1);
-    assertQuery("committer:example.com", change1);
+    // Right combination of email address and name.
+    assertQuery(searchOperator + "\"John Doe <john.doe@example.com>\"", change1);
+    assertQuery(searchOperator + "\" John <john@example.com> \"", change2);
+    assertQuery(searchOperator + "\"doE SMITH <doe_smitH@example.com>\"", change3);
 
-    // By name part
-    assertQuery("committer:Committer", change1);
+    // Wrong combination of email address and name.
+    assertQuery(searchOperator + "\"John <john.doe@example.com>\"");
+    assertQuery(searchOperator + "\"Doe John <john@example.com>\"");
+    assertQuery(searchOperator + "\"Doe John <doe_smith@example.com>\"");
+  }
 
-    // Case insensitive
-    assertQuery("committer:jCoMmItTeR", change1);
-    assertQuery("committer:ExAmPlE", change1);
+  private void byAuthorOrCommitterFullText(String searchOperator) throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
+    PersonIdent john = new PersonIdent("John", "john@example.com");
+    PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
+    Change change1 = createChange(repo, johnDoe);
+    Change change2 = createChange(repo, john);
+    Change change3 = createChange(repo, doeSmith);
 
-    // By non-existing email address / name / part
-    assertQuery("committer:jauthor@example.com");
-    assertQuery("committer:somewhere.com");
-    assertQuery("committer:jauthor");
-    assertQuery("committer:Author");
+    // By exact name.
+    assertQuery(searchOperator + "\"John Doe\"", change1);
+    assertQuery(searchOperator + "\"john\"", change2, change1);
+    assertQuery(searchOperator + "\"Doe smith\"", change3);
+
+    // By name part.
+    assertQuery(searchOperator + "Doe", change3, change1);
+    assertQuery(searchOperator + "smith", change3);
+
+    // By wrong combination.
+    assertQuery(searchOperator + "\"John Smith\"");
+
+    // By invalid query.
+    exception.expect(BadRequestException.class);
+    exception.expectMessage("invalid value");
+    // SchemaUtil.getNameParts will return an empty set for query only containing these characters.
+    assertQuery(searchOperator + "@.- /_");
+  }
+
+  private Change createChange(TestRepository<Repo> repo, PersonIdent person) throws Exception {
+    RevCommit commit =
+        repo.parseBody(repo.commit().message("message").author(person).committer(person).create());
+    return insert(repo, newChangeForCommit(repo, commit), null);
   }
 
   @Test
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index ce444c5..01f61d9 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -2,8 +2,10 @@
 
 ## Installing [Node.js](https://nodejs.org/en/download/)
 
+The minimum nodejs version supported is 6.x+
+
 ```sh
-# Debian/Ubuntu
+# Debian experimental
 sudo apt-get install nodejs-legacy
 
 # OS X with Homebrew
@@ -146,3 +148,17 @@
 `eslint --ext .html,.js polygerrit-ui/app/$YOUR_DIR_HERE`
 * To run the linter on all of your local changes:
 `git diff --name-only master | xargs eslint --ext .html,.js`
+
+We also use the polylint tool to lint use of Polymer. To install polylint,
+execute the following command.
+
+```sh
+npm install -g polylint
+```
+
+To run polylint, execute the following command.
+
+```sh
+bazel test //polygerrit-ui/app:polylint_test
+```
+
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 4e99272..b2ce8f0 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -129,6 +129,16 @@
     ),
 )
 
+filegroup(
+    name = "bower_components",
+    srcs = glob(
+        [
+            "bower_components/**/*.html",
+            "bower_components/**/*.js",
+        ]
+    ),
+)
+
 genrule2(
     name = "pg_code_zip",
     srcs = [":pg_code"],
@@ -172,3 +182,18 @@
         "manual",
     ],
 )
+
+sh_test(
+    name = "polylint_test",
+    size = "large",
+    srcs = ["polylint_test.sh"],
+    data = [
+        ":pg_code",
+        ":bower_components",
+    ],
+    # Should not run sandboxed.
+    tags = [
+        "local",
+        "manual",
+    ],
+)
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
index adc9810..65908f2 100644
--- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -18,10 +18,10 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
 
-<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-change-table-behavior.html">
 
 <test-fixture id="basic">
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index aefbcb8..7ebb17a 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -14,7 +14,7 @@
 limitations under the License.
 -->
 <!-- Polymer included for the html import polyfill. -->
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../bower_components/web-component-tester/browser.js"></script>
 <title>gr-patch-set-behavior</title>
 
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 7b94ab6..848c744 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
@@ -14,7 +14,7 @@
 limitations under the License.
 -->
 <!-- Polymer included for the html import polyfill. -->
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<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/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
index a026ff9..df376ba 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -18,8 +18,8 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>keyboard-shortcut-behavior</title>
 
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="keyboard-shortcut-behavior.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 2cf8c41..4ced26e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -124,6 +124,9 @@
         overflow: auto;
         word-break: break-all;
       }
+      #commitMessageEditor {
+        min-width: 72ch;
+      }
       .editCommitMessage {
         margin-top: 1em;
       }
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 f1613e5..80bf06c 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
@@ -36,6 +36,7 @@
         display: block;
       }
       .row {
+        border-top: 1px solid #eee;
         display: flex;
         padding: .1em .25em;
       }
@@ -334,8 +335,10 @@
         </template>
       </template>
     </div>
-    <div class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]">
-      <div class="total-stats" hidden$="[[_hideChangeTotals]]">
+    <div
+        class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]"
+        hidden$="[[_hideChangeTotals]]">
+      <div class="total-stats">
         <span
             class="added"
             tabindex="0"
@@ -350,8 +353,10 @@
         </span>
       </div>
     </div>
-    <div class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]">
-      <div class="total-stats" hidden$="[[_hideBinaryChangeTotals]]">
+    <div
+        class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]"
+        hidden$="[[_hideBinaryChangeTotals]]">
+      <div class="total-stats">
         <span class="added" aria-label="Total lines added">
           [[_formatBytes(_patchChange.size_delta_inserted)]]
           [[_formatPercentage(_patchChange.total_size,
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 b167404..ac8e299 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
@@ -475,7 +475,9 @@
 
     _handleCommentDelete() {
       Polymer.dom(Gerrit.getRootElement()).appendChild(this.$.overlay);
-      this.$.overlay.open();
+      this.async(() => {
+        this.$.overlay.open();
+      }, 1);
     },
 
     _handleCancelDeleteComment() {
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 893b7ed..ce3b233 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
@@ -269,12 +269,14 @@
       assert.isTrue(element.$$('.action.delete')
           .classList.contains('showDeleteButtons'));
       MockInteractions.tap(element.$$('.action.delete'));
-      element.$.overlay.open.lastCall.returnValue.then(() => {
-        element.$.confirmDeleteComment.message = 'removal reason';
-        element._handleConfirmDeleteComment();
-        assert.isTrue(element.$.restAPI.deleteComment.calledWith(
-            42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
-        done();
+      flush(() => {
+        element.$.overlay.open.lastCall.returnValue.then(() => {
+          element.$.confirmDeleteComment.message = 'removal reason';
+          element._handleConfirmDeleteComment();
+          assert.isTrue(element.$.restAPI.deleteComment.calledWith(
+              42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
+          done();
+        });
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index 81379af..afbce45 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -43,13 +43,16 @@
         on-keydown="_handleKeydown"
         on-focus="_onInputFocus"
         autocomplete="off" />
-    <gr-autocomplete-dropdown id="suggestions"
-        on-item-selected="_handleItemSelect"
-        suggestions="[[_suggestions]]"
-        role="listbox"
-        index="[[index]]"
-        hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]">
-    </gr-autocomplete-dropdown>
+    <!-- This container is needed for Safari and Firefox -->
+    <div id="suggestionContainer">
+      <gr-autocomplete-dropdown id="suggestions"
+          on-item-selected="_handleItemSelect"
+          suggestions="[[_suggestions]]"
+          role="listbox"
+          index="[[index]]"
+          hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]">
+      </gr-autocomplete-dropdown>
+    </div>
   </template>
   <script src="gr-autocomplete.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index f83e856..c92a479 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -70,12 +70,16 @@
     });
 
     test('emoji selector is not open with the textarea lacks focus', () => {
+      element.$.textarea.selectionStart = 1;
+      element.$.textarea.selectionEnd = 1;
       element.text = ':';
       assert.isFalse(!element.$.emojiSuggestions.hidden);
     });
 
     test('emoji selector is not open when a general text is entered', () => {
       MockInteractions.focus(element.$.textarea);
+      element.$.textarea.selectionStart = 9;
+      element.$.textarea.selectionEnd = 9;
       element.text = 'some text';
       assert.isFalse(!element.$.emojiSuggestions.hidden);
     });
@@ -83,10 +87,16 @@
     test('emoji selector opens when a colon is typed & the textarea has focus',
         () => {
           MockInteractions.focus(element.$.textarea);
-          flushAsynchronousOperations();
+          // Needed for Safari tests. selectionStart is not updated when text is
+          // updated.
+          element.$.textarea.selectionStart = 1;
+          element.$.textarea.selectionEnd = 1;
           element.text = ':';
+          element.$.textarea.selectionStart = 2;
+          element.$.textarea.selectionEnd = 2;
           element.text = ':t';
-          assert.isTrue(!element.$.emojiSuggestions.hidden);
+          flushAsynchronousOperations();
+          assert.isFalse(element.$.emojiSuggestions.hidden);
           assert.equal(element._colonIndex, 0);
           assert.isFalse(element._hideAutocomplete);
           assert.equal(element._currentSearchString, 't');
@@ -96,8 +106,14 @@
       const resetStub = sandbox.stub(element, '_resetEmojiDropdown');
       MockInteractions.focus(element.$.textarea);
       flushAsynchronousOperations();
+      element.$.textarea.selectionStart = 10;
+      element.$.textarea.selectionEnd = 10;
       element.text = 'test test ';
+      element.$.textarea.selectionStart = 12;
+      element.$.textarea.selectionEnd = 12;
       element.text = 'test test :';
+      element.$.textarea.selectionStart = 15;
+      element.$.textarea.selectionEnd = 15;
       element.text = 'test test :smi';
 
       assert.equal(element._currentSearchString, 'smi');
@@ -140,6 +156,8 @@
     });
 
     test('_handleEmojiSelect', () => {
+      element.$.textarea.selectionStart = 16;
+      element.$.textarea.selectionEnd = 16;
       element.text = 'test test :tears';
       element._colonIndex = 10;
       const selectedItem = {dataset: {value: '😂'}};
@@ -149,6 +167,8 @@
     });
 
     test('_getPositionOfCursor', () => {
+      element.$.textarea.selectionStart = 4;
+      element.$.textarea.selectionEnd = 4;
       element.text = 'test';
       element._getPositionOfCursor();
       assert.deepEqual(element.$.hiddenText.innerHTML, element.text +
@@ -178,7 +198,11 @@
       function setupDropdown() {
         MockInteractions.focus(element.$.textarea);
         flushAsynchronousOperations();
+        element.$.textarea.selectionStart = 1;
+        element.$.textarea.selectionEnd = 1;
         element.text = ':';
+        element.$.textarea.selectionStart = 1;
+        element.$.textarea.selectionEnd = 1;
         element.text = ':1';
       }
 
diff --git a/polygerrit-ui/app/lint_test.sh b/polygerrit-ui/app/lint_test.sh
index 7ee74d8..35939ba 100755
--- a/polygerrit-ui/app/lint_test.sh
+++ b/polygerrit-ui/app/lint_test.sh
@@ -3,7 +3,7 @@
 set -ex
 
 eslint_bin=$(which npm)
-if [[ -z "$eslint_bin" ]]; then
+if [ -z "$eslint_bin" ]; then
     echo "NPM must be on the path."
     exit 1
 fi
@@ -11,7 +11,7 @@
 eslint_bin=$(which eslint)
 eslint_config=$(npm list -g | grep -c eslint-config-google)
 eslint_plugin=$(npm list -g | grep -c eslint-plugin-html)
-if [[ -z "$eslint_bin" ]] || [[ eslint_config -eq "0" ]] || [[ eslint_plugin -eq "0" ]]; then
+if [ -z "$eslint_bin" ] || [ "$eslint_config" -eq "0" ] || [ "$eslint_plugin" -eq "0" ]; then
     echo "You must install ESLint and its dependencies from NPM."
     echo "> npm install -g eslint eslint-config-google eslint-plugin-html"
     echo "For more information, view the README:"
diff --git a/polygerrit-ui/app/polylint_test.sh b/polygerrit-ui/app/polylint_test.sh
new file mode 100755
index 0000000..9f136a6
--- /dev/null
+++ b/polygerrit-ui/app/polylint_test.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+set -ex
+
+npm_bin=$(which npm)
+if [[ -z "$npm_bin" ]]; then
+    echo "NPM must be on the path."
+    exit 1
+fi
+
+polylint_bin=$(which polylint)
+if [[ -z "$polylint_bin" ]]; then
+    echo "You must install polylint and its dependencies from NPM."
+    echo "> npm install -g polylint"
+    exit 1
+fi
+
+${polylint_bin} --root polygerrit-ui/app --input elements/gr-app.html