Merge "ProjectAccess: increase font sizes for project rights"
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 6efabff..dd230d3 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -164,7 +164,8 @@
     contributors may also like to open several editors side by
     side while editing new changes.
   * Use 2 spaces for indent (no tabs)
-  * Use brackets in all ifs, spaces before/after if parens.
+  * Use braces in all if/else/for/do/while/catch blocks, spaces before/after
+    if/for/while/catch parens.
   * Use /** */ style Javadocs for variables.
 
 Additionally, you will notice that most of the newline spacing
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index e32e30d..2870cf8 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -415,6 +415,10 @@
 +
 Garbage collection ran on a project
 
+* `com.google.gerrit.server.extensions.events.ChangeIndexedListener`:
++
+Update of the secondary index
+
 [[stream-events]]
 == Sending Events to the Events Stream
 
@@ -433,6 +437,13 @@
 `com.google.gerrit.server.events.EventDeserializer` class requires
 that the event be registered in EventTypes.
 
+== Modifying the Stream Event Flow
+
+It is possible to modify the stream event flow from plugins by registering
+an `com.google.gerrit.server.events.EventDispatcher`. A plugin may register
+a Dispatcher class to replace the internal Dispatcher. EventDispatcher is
+a DynamicItem, so Gerrit may only have one copy.
+
 [[validation]]
 == Validation Listeners
 
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 3157214..2a6cb1c 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -165,6 +165,15 @@
 * Sanity check WAR
 * Test the new Gerrit version
 
+* Verify plugin versions
++
+Sometimes `buck` doesn't rebuild plugins after they are tagged, and the
+versions don't reflect the tag. Verify the versions:
++
+----
+  java -jar ./buck-out/gen/release/release.war init --list-plugins
+----
+
 [[publish-gerrit]]
 === Publish the Gerrit Release
 
@@ -336,16 +345,11 @@
   make -C ReleaseNotes
 ----
 
-* Build the documentation:
-+
-----
-  buck build docs
-----
+* Extract the documentation files from the zip file generated during
+the release build: `buck-out/gen/Documentation/html/html.zip`.
 
-* Extract the documentation html files from the generated zip file
-`buck-out/gen/Documentation/searchfree.zip`.
-
-* Upload the html files manually via web browser to the
+* Upload the files manually via web browser to the appropriate folder
+in the
 link:https://console.developers.google.com/project/164060093628/storage/gerrit-documentation/[
 gerrit-documentation] storage bucket.
 
@@ -383,13 +387,12 @@
 ** A link to the release and the release notes (if a final release)
 ** A link to the docs
 ** Describe the type of release (stable, bug fix, RC)
-
-* Add an entry to the `NEWS` section of the main Gerrit project web page
-** Go to: http://code.google.com/p/gerrit/admin
-** Add entry like:
-----
- * Jun 14, 2012 - Gerrit 2.4.1 [https://groups.google.com/d/topic/repo-discuss/jHg43gixqzs/discussion Released]
-----
+** Hash values (SHA1, SHA256, MD5) for the release WAR file.
++
+The SHA1 and MD5 can be taken from the artifact page on Sonatype. The
+SHA256 can be generated with
+`openssl sha -sha256 buck-out/gen/release/release.war` or an equivalent
+command.
 
 * Update the new discussion group announcement to be sticky
 ** Go to: http://groups.google.com/group/repo-discuss/topics
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 446c3c6..5603d36 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1050,8 +1050,7 @@
 Submits a change.
 
 The request body only needs to include a link:#submit-input[
-SubmitInput] entity if the request should wait for the merge to
-complete.
+SubmitInput] entity if submitting on behalf of another user.
 
 .Request
 ----
@@ -1059,7 +1058,7 @@
   Content-Type: application/json; charset=UTF-8
 
   {
-    "wait_for_merge": true
+    "on_behalf_of": 1001439
   }
 ----
 
@@ -4780,10 +4779,6 @@
 |Field Name    ||Description
 |`status`      ||
 The status of the change after submitting is `MERGED`.
-+
-As `wait_for_merge` in the link:#submit-input[SubmitInput] is deprecated and
-the request always waits for the merge to be completed, you can expect
-`MERGED` to be returned here.
 |`on_behalf_of`|optional|
 The link:rest-api-accounts.html#account-id[\{account-id\}] of the user on
 whose behalf the action should be done. To use this option the caller must
@@ -4808,8 +4803,6 @@
 API]. Using this option requires
 link:access-control.html#category_submit_on_behalf_of[Submit (On Behalf Of)]
 permission on the branch.
-|`wait_for_merge`|Deprecated, always `true`|
-Whether the request should wait for the merge to complete.
 |===========================
 
 [[submit-record]]
diff --git a/Documentation/user-review-ui.txt b/Documentation/user-review-ui.txt
index 57dca8c..8553634 100644
--- a/Documentation/user-review-ui.txt
+++ b/Documentation/user-review-ui.txt
@@ -186,10 +186,6 @@
 The `Submit` button is available if the change is submittable and
 the link:access-control.html#category_submit[Submit] access right is
 assigned.
-+
-It is also possible to submit changes that have merge conflicts. This
-allows to do the conflict resolution for a change series in a single
-merge commit and submit the changes in reverse order.
 
 ** [[abandon]]`Abandon`:
 +
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index eda4b5d..b2b3614 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -299,7 +299,7 @@
 +
 True if the change is either merged or abandoned.
 
-is:submitted, is:merged, is:abandoned::
+is:merged, is:abandoned::
 +
 Same as <<status,status:'STATE'>>.
 
@@ -320,10 +320,6 @@
 more recently than the last update (comment or patch set) from the
 change owner.
 
-status:submitted::
-+
-Change has been submitted, but is waiting for a dependency.
-
 status:closed::
 +
 True if the change is either 'merged' or 'abandoned'.
diff --git a/ReleaseNotes/ReleaseNotes-2.11.8.txt b/ReleaseNotes/ReleaseNotes-2.11.8.txt
new file mode 100644
index 0000000..0f9dc21
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.8.txt
@@ -0,0 +1,43 @@
+Release notes for Gerrit 2.11.8
+===============================
+
+Gerrit 2.11.8 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.8.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.11.8.war]
+
+There are no schema changes from link:ReleaseNotes-2.11.7.html[2.11.7].
+
+Bug Fixes
+---------
+
+* Upgrade Apache commons-collections to version 3.2.2.
++
+Includes a fix for a link:https://issues.apache.org/jira/browse/COLLECTIONS-580[
+remote code execution exploit].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1207[Issue 1207]:
+Fix keyboard shortcuts for non-US keyboards on side-by-side diff screen.
++
+The forward/backward navigation keys `[` and `]` only worked on keyboards where
+these characters could be typed without using any modifier key (like CTRL, ALT,
+etc.).
++
+Note that the problem still exists on the unified diff screen.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3919[Issue 3919]:
+Explicitly set parent project to 'All-Projects' when a project is created
+without giving the parent.
+
+* Don't add message twice on abandon or restore via ssh review command.
++
+When abandoning or reviewing a change via the ssh `review` command, and
+providing a message with the `--message` option, the message was added to
+the change twice.
+
+* Clear the input box after cancelling add reviewer action.
++
+When the action was cancelled, the content of the input box was still
+there when opening it again.
+
+* Fix internal server error when aborting ssh command.
diff --git a/ReleaseNotes/ReleaseNotes-2.12.1.txt b/ReleaseNotes/ReleaseNotes-2.12.1.txt
index 379572e..f49de7d 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.1.txt
@@ -11,31 +11,48 @@
 link:ReleaseNotes-2.11.7.html[Gerrit 2.11.7]. These bug fixes are *not*
 listed in these release notes.
 
+Schema Upgrade
+--------------
+
 *WARNING:* This version includes a manual schema upgrade when upgrading
 from 2.12.
-+
-If you have already upgraded to 2.12, you need to issue this SQL statement
-manually (e.g. using the `gerrit gsql` SSH command or the `gqsl` site
-program):
-+
+
+When upgrading a site that is already running version 2.12, the `patch_sets`
+table must be manually migrated using the `gerrit gsql` SSH command or the
+`gqsl` site program.
+
+For the default H2 database, execute the command:
+
+----
   alter table patch_sets modify push_certficate clob;
-+
-Or with this command if the site is configured to use PostgreSQL:
-+
+----
+
+For MySQL, execute the command:
+
+----
+  alter table patch_sets modify push_certficate text;
+----
+
+For PostgreSQL, execute the command:
+
+----
   alter table patch_sets alter column push_certficate type text;
-+
+----
+
+For other database types, execute the appropriate equivalent command.
+
 Note that the misspelled `push_certficate` is the actual name of the
 column.
-+
-If you are upgrading from a version earlier than 2.12, this manual step is
-not necessary and should be omitted.
+
+When upgrading from a version earlier than 2.12, this manual step is not
+necessary and should be omitted.
 
 
 Bug Fixes
 ---------
 
 General
-^^^^^^^
+~~~~~~~
 
 * Fix column type for signed push certificates.
 +
@@ -69,7 +86,7 @@
 'Internal Server Error' that was difficult to track down. Now an error is
 raised earlier which will help administrators to find the root cause.
 
-* https://code.google.com/p/gerrit/issues/detail?id=3743[Issue 3743]:
+* link:https://code.google.com/p/gerrit/issues/detail?id=3743[Issue 3743]:
 Use submitter identity as committer when using 'Rebase if Necessary' merge
 strategy.
 +
@@ -125,8 +142,29 @@
 Explicitly set parent project to 'All-Projects' when a project is created
 without giving the parent.
 
+* link:https://code.google.com/p/gerrit/issues/detail?id=3948[Issue 3948]:
+Fix submit of project parent updates on `refs/meta/config`.
++
+When submitting a change on `refs/meta/config` to update a project's parent,
+the error 'The change must be submitted by a Gerrit administrator' was being
+displayed even when the submitter was an admin. The submit was successful
+when clicking 'Submit' a second time.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3811[Issue 3811]:
+Fix submittability of merge commits that resolve merge conflicts.
++
+If a series of changes contained a change that conflicted with the destination
+branch, but the conflict was solved by a merge commit at the tip of the
+series, the series was not submittable.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3883[Issue 3883]:
+Respect the `core.commentchar` setting from `.gitconfig` in `commit-msg` hook.
+
 UI
-^^
+~~
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3894[Issue 3894]:
+Fix display of 'Related changes' after change is rebased in web UI:
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3071[Issue 3071]:
 Fix display of submodule differences in side-by-side view.
@@ -153,22 +191,11 @@
 +
 Note that the problem still exists on the unified diff screen.
 
+* Improve tooltip on 'Submit' button when 'Submit whole topic' is enabled
+and the topic can't be submitted due to some changes not being ready.
+
 Plugins
-^^^^^^^
-
-* Allow plugins to get the caller in merge validation requests.
-+
-Plugins that implement the `MergeValidationListener` interface now get the
-caller (the user who initiated the merge) in the `onPreMerge` method.
-+
-Existing plugins that implement this interface must be adapted to the new
-method signature.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3741[Issue 3741]:
-Fix handling of merge validation exceptions emitted by plugins.
-+
-If a plugin raised an exception, it was reported to the user as 'Change is
-new', rather than 'Missing dependency'.
+~~~~~~~
 
 * link:https://code.google.com/p/gerrit/issues/detail?id=3821[Issue 3821]:
 Fix repeated reloading of plugins when running on OpenJDK 8.
@@ -178,11 +205,31 @@
 comparing the plugin's JAR file timestamp, resulting in the plugin being
 reloaded every minute.
 
+* link:https://code.google.com/p/gerrit/issues/detail?id=3741[Issue 3741]:
+Fix handling of merge validation exceptions emitted by plugins.
++
+If a plugin raised an exception, it was reported to the user as 'Change is
+new', rather than 'Missing dependency'.
+
+* Allow plugins to get the caller in merge validation requests.
++
+Plugins that implement the `MergeValidationListener` interface now get the
+caller (the user who initiated the merge) in the `onPreMerge` method.
++
+Existing plugins that implement this interface must be adapted to the new
+method signature.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3892[Issue 3892]:
+Allow plugins to suggest reviewers based on either change or project
+resources.
+
 Documentation
-^^^^^^^^^^^^^
+~~~~~~~~~~~~~
 
 * Update documentation of `commentlink` to reflect changed search URL.
 
+* Add missing documentation of valid `database.type` values.
+
 Upgrades
 --------
 
diff --git a/ReleaseNotes/ReleaseNotes-2.12.2.txt b/ReleaseNotes/ReleaseNotes-2.12.2.txt
new file mode 100644
index 0000000..5582bf9
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.2.txt
@@ -0,0 +1,38 @@
+Release notes for Gerrit 2.12.2
+===============================
+
+Gerrit 2.12.2 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war]
+
+There are no schema changes from link:ReleaseNotes-2.12.1.html[2.12.1].
+
+Bug Fixes
+---------
+
+* Upgrade Apache commons-collections to version 3.2.2.
++
+Includes a fix for a link:https://issues.apache.org/jira/browse/COLLECTIONS-580[
+remote code execution exploit].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3919[Issue 3919]:
+Explicitly set parent project to 'All-Projects' when a project is created
+without giving the parent.
+
+* Don't add message twice on abandon or restore via ssh review command.
++
+When abandoning or reviewing a change via the ssh `review` command, and
+providing a message with the `--message` option, the message was added to
+the change twice.
+
+* Clear the input box after cancelling add reviewer action.
++
+When the action was cancelled, the content of the input box was still
+there when opening it again.
+
+* Fix internal server error when aborting ssh command.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3969[Issue 3969]:
+Fix internal server error when submitting a change with 'Rebase If Necessary'
+strategy.
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index af33d7a..4cab151 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -9,12 +9,14 @@
 [[2_12]]
 Version 2.12.x
 --------------
+* link:ReleaseNotes-2.12.2.html[2.12.2]
 * link:ReleaseNotes-2.12.1.html[2.12.1]
 * link:ReleaseNotes-2.12.html[2.12]
 
 [[2_11]]
 Version 2.11.x
 --------------
+* link:ReleaseNotes-2.11.8.html[2.11.8]
 * link:ReleaseNotes-2.11.7.html[2.11.7]
 * link:ReleaseNotes-2.11.6.html[2.11.6]
 * link:ReleaseNotes-2.11.5.html[2.11.5]
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 c1b2eb9..1172d16 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
@@ -69,6 +69,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.FakeEmailSender;
 import com.google.gerrit.testutil.TempFileUtil;
 import com.google.gerrit.testutil.TestNotesMigration;
 import com.google.gson.Gson;
@@ -187,6 +188,9 @@
   @Inject
   protected Revisions revisions;
 
+  @Inject
+  protected FakeEmailSender sender;
+
   protected TestRepository<InMemoryRepository> testRepo;
   protected GerritServer server;
   protected TestAccount admin;
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
index 4a6d22d..7f08b6f 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Function;
 import com.google.common.collect.FluentIterable;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.mail.Address;
 
 import com.jcraft.jsch.KeyPair;
 
@@ -58,6 +59,7 @@
   public final Account.Id id;
   public final String username;
   public final String email;
+  public final Address emailAddress;
   public final String fullName;
   public final KeyPair sshKey;
   public final String httpPassword;
@@ -67,6 +69,7 @@
     this.id = id;
     this.username = username;
     this.email = email;
+    this.emailAddress = new Address(fullName, email);
     this.fullName = fullName;
     this.sshKey = sshKey;
     this.httpPassword = httpPassword;
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 a5274fa..f2886e1 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
@@ -67,6 +67,7 @@
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.Util;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
 
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -512,6 +513,14 @@
         .id(r.getChangeId())
         .addReviewer(in);
 
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.body()).contains("I'd like you to do a code review.");
+    assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+
     ChangeInfo c = gApi.changes()
         .id(r.getChangeId())
         .get();
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 7add9a2..def8317 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
@@ -19,6 +19,7 @@
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.HEAD;
 import static org.junit.Assert.fail;
@@ -29,6 +30,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+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;
@@ -38,6 +40,7 @@
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 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.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -49,9 +52,13 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ETagView;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.server.change.GetRevisionActions;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -119,18 +126,68 @@
   @Test
   public void submit() throws Exception {
     PushOneCommit.Result r = createChange();
+    String changeId = project.get() + "~master~" + r.getChangeId();
     gApi.changes()
-        .id(project.get() + "~master~" + r.getChangeId())
+        .id(changeId)
         .current()
         .review(ReviewInput.approve());
     gApi.changes()
-        .id(project.get() + "~master~" + r.getChangeId())
+        .id(changeId)
         .current()
         .submit();
+    assertThat(gApi.changes().id(changeId).get().status)
+        .isEqualTo(ChangeStatus.MERGED);
   }
 
-  @Test(expected = AuthException.class)
+  private void allowSubmitOnBehalfOf() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.allow(cfg,
+        Permission.SUBMIT_AS,
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
+        "refs/heads/*");
+    saveProjectConfig(project, cfg);
+  }
+
+  @Test
   public void submitOnBehalfOf() throws Exception {
+    allowSubmitOnBehalfOf();
+    PushOneCommit.Result r = createChange();
+    String changeId = project.get() + "~master~" + r.getChangeId();
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .review(ReviewInput.approve());
+    SubmitInput in = new SubmitInput();
+    in.onBehalfOf = admin2.email;
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .submit(in);
+    assertThat(gApi.changes().id(changeId).get().status)
+        .isEqualTo(ChangeStatus.MERGED);
+  }
+
+  @Test
+  public void submitOnBehalfOfInvalidUser() throws Exception {
+    allowSubmitOnBehalfOf();
+    PushOneCommit.Result r = createChange();
+    String changeId = project.get() + "~master~" + r.getChangeId();
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .review(ReviewInput.approve());
+    SubmitInput in = new SubmitInput();
+    in.onBehalfOf = "doesnotexist";
+    exception.expect(UnprocessableEntityException.class);
+    exception.expectMessage("Account Not Found: doesnotexist");
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .submit(in);
+  }
+
+  @Test
+  public void submitOnBehalfOfNotPermitted() throws Exception {
     PushOneCommit.Result r = createChange();
     gApi.changes()
         .id(project.get() + "~master~" + r.getChangeId())
@@ -138,6 +195,8 @@
         .review(ReviewInput.approve());
     SubmitInput in = new SubmitInput();
     in.onBehalfOf = admin2.email;
+    exception.expect(AuthException.class);
+    exception.expectMessage("submit on behalf of not permitted");
     gApi.changes()
         .id(project.get() + "~master~" + r.getChangeId())
         .current()
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 442eecd..3e7d0d5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.EditInfo;
@@ -42,6 +43,7 @@
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.testutil.TestTimeUtil;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
 
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.AfterClass;
@@ -127,6 +129,40 @@
   }
 
   @Test
+  public void testPushForMasterWithNotify() throws Exception {
+    TestAccount user2 = accounts.user2();
+    String pushSpec = "refs/for/master"
+        + "%reviewer=" + user.email
+        + ",cc=" + user2.email;
+
+    sender.clear();
+    PushOneCommit.Result r =
+        pushTo(pushSpec + ",notify=" + NotifyHandling.NONE);
+    r.assertOkStatus();
+    assertThat(sender.getMessages()).hasSize(0);
+
+    sender.clear();
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.OWNER);
+    r.assertOkStatus();
+    // no email notification about own changes
+    assertThat(sender.getMessages()).hasSize(0);
+
+    sender.clear();
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.OWNER_REVIEWERS);
+    r.assertOkStatus();
+    assertThat(sender.getMessages()).hasSize(1);
+    Message m = sender.getMessages().get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+
+    sender.clear();
+    r = pushTo(pushSpec + ",notify=" + NotifyHandling.ALL);
+    r.assertOkStatus();
+    assertThat(sender.getMessages()).hasSize(1);
+    m = sender.getMessages().get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress, user2.emailAddress);
+  }
+
+  @Test
   public void testPushForMasterWithCc() throws Exception {
     // cc one user
     String topic = "my/topic";
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index e5281b1..e26747b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -103,7 +103,7 @@
       assertThat(info.label).isEqualTo("Submit whole topic");
       assertThat(info.method).isEqualTo("POST");
       assertThat(info.title).isEqualTo(
-          "See the \"Submitted Together\" tab for problems");
+          "See the \"Submitted Together\" tab for problems, specially see: 2");
     } else {
       noSubmitWholeTopicAssertions(actions, 1);
     }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
new file mode 100644
index 0000000..c4216cd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -0,0 +1,154 @@
+// 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.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.Util;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefSpec;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ConfigChangeIT extends AbstractDaemonTest {
+  @Before
+  public void setUp() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    Util.allow(cfg, Permission.OWNER, REGISTERED_USERS, "refs/*");
+    Util.allow(
+        cfg, Permission.PUSH, REGISTERED_USERS, "refs/for/refs/meta/config");
+    Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/meta/config");
+    saveProjectConfig(project, cfg);
+
+    setApiUser(user);
+    fetchRefsMetaConfig();
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void updateProjectConfig() throws Exception {
+    Config cfg = readProjectConfig();
+    assertThat(cfg.getString("project", null, "description")).isNull();
+    String desc = "new project description";
+    cfg.setString("project", null, "description", desc);
+
+    PushOneCommit.Result r = createConfigChange(cfg);
+    String id = r.getChangeId();
+
+    gApi.changes().id(id).current().review(ReviewInput.approve());
+    gApi.changes().id(id).current().submit();
+
+    assertThat(gApi.changes().id(id).info().status)
+        .isEqualTo(ChangeStatus.MERGED);
+    assertThat(gApi.projects().name(project.get()).get().description)
+        .isEqualTo(desc);
+    fetchRefsMetaConfig();
+    assertThat(readProjectConfig().getString("project", null, "description"))
+        .isEqualTo(desc);
+  }
+
+  @Test
+  @TestProjectInput(cloneAs = "user")
+  public void onlyAdminMayUpdateProjectParent() throws Exception {
+    setApiUser(admin);
+    ProjectInput parent = new ProjectInput();
+    parent.name = name("parent");
+    parent.permissionsOnly = true;
+    gApi.projects().create(parent);
+
+    setApiUser(user);
+    Config cfg = readProjectConfig();
+    assertThat(cfg.getString("access", null, "inheritFrom"))
+        .isAnyOf(null, allProjects.get());
+    cfg.setString("access", null, "inheritFrom", parent.name);
+
+    PushOneCommit.Result r = createConfigChange(cfg);
+    String id = r.getChangeId();
+
+    gApi.changes().id(id).current().review(ReviewInput.approve());
+    try {
+      gApi.changes().id(id).current().submit();
+      fail("expected submit to fail");
+    } catch (ResourceConflictException e) {
+      int n = gApi.changes().id(id).info()._number;
+      assertThat(e).hasMessage(
+          "Failed to submit 1 change due to the following problems:\n"
+          + "Change " + n + ": Change contains a project configuration that"
+          +" changes the parent project.\n"
+          + "The change must be submitted by a Gerrit administrator.");
+    }
+
+    assertThat(gApi.projects().name(project.get()).get().parent)
+        .isEqualTo(allProjects.get());
+    fetchRefsMetaConfig();
+    assertThat(readProjectConfig().getString("access", null, "inheritFrom"))
+        .isAnyOf(null, allProjects.get());
+
+    setApiUser(admin);
+    gApi.changes().id(id).current().submit();
+    assertThat(gApi.changes().id(id).info().status)
+        .isEqualTo(ChangeStatus.MERGED);
+    assertThat(gApi.projects().name(project.get()).get().parent)
+        .isEqualTo(parent.name);
+    fetchRefsMetaConfig();
+    assertThat(readProjectConfig().getString("access", null, "inheritFrom"))
+        .isEqualTo(parent.name);
+  }
+
+  private void fetchRefsMetaConfig() throws Exception {
+    git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config"))
+        .call();
+    testRepo.reset("refs/meta/config");
+  }
+
+  private Config readProjectConfig() throws Exception {
+    RevWalk rw = testRepo.getRevWalk();
+    RevTree tree = rw.parseTree(testRepo.getRepository().resolve("HEAD"));
+    RevObject obj = rw.parseAny(testRepo.get(tree, "project.config"));
+    ObjectLoader loader = rw.getObjectReader().open(obj);
+    String text = new String(loader.getCachedBytes(), UTF_8);
+    Config cfg = new Config();
+    cfg.fromText(text);
+    return cfg;
+  }
+
+  private PushOneCommit.Result createConfigChange(Config cfg) throws Exception {
+    PushOneCommit.Result r = pushFactory.create(
+            db, user.getIdent(), testRepo,
+            "Update project config",
+            "project.config",
+            cfg.toText())
+        .to("refs/for/refs/meta/config");
+    r.assertOkStatus();
+    return r;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index a7c75ef..5a6c36a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -294,7 +294,7 @@
       OrmException {
     ChangeSet cs =
         mergeSuperSet.completeChangeSet(db, change.change(), user(admin));
-    assertThat(submit.isPatchSetMergeable(cs)).isEqualTo(expected);
+    assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
   }
 
   private void assertMergeable(ChangeData change, boolean expected)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 0b86ad9..fd352d1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -23,9 +23,7 @@
 import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.testutil.FakeEmailSender;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
-import com.google.inject.Inject;
 
 import org.junit.Test;
 
@@ -34,9 +32,6 @@
 
 @NoHttpd
 public class ProjectWatchIT extends AbstractDaemonTest {
-  @Inject
-  private FakeEmailSender sender;
-
   /**
    * Tests message project watches on new patch sets
    * <p>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
new file mode 100644
index 0000000..e07405f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/AbandonRestoreIT.java
@@ -0,0 +1,89 @@
+// 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.acceptance.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+@NoHttpd
+public class AbandonRestoreIT extends AbstractDaemonTest {
+
+  @Test
+  public void withMessage() throws Exception {
+    Result result = createChange();
+    String commit = result.getCommit().name();
+    executeCmd(commit, "abandon", "'abandon it'");
+    executeCmd(commit, "restore", "'restore it'");
+    assertChangeMessages(result.getChangeId(), ImmutableList.of(
+        "Uploaded patch set 1.",
+        "Abandoned\n\nabandon it",
+        "Restored\n\nrestore it"));
+  }
+
+  @Test
+  public void withoutMessage() throws Exception {
+    Result result = createChange();
+    String commit = result.getCommit().name();
+    executeCmd(commit, "abandon", null);
+    executeCmd(commit, "restore", null);
+    assertChangeMessages(result.getChangeId(), ImmutableList.of(
+        "Uploaded patch set 1.",
+        "Abandoned",
+        "Restored"));
+  }
+
+  private void executeCmd(String commit, String op, String message)
+      throws Exception {
+    StringBuilder command = new StringBuilder("gerrit review ")
+        .append(commit)
+        .append(" --")
+        .append(op);
+    if (message != null) {
+      command.append(" --message ").append(message);
+    }
+    String response = sshSession.exec(command.toString());
+    assert_()
+      .withFailureMessage(sshSession.getError())
+      .that(sshSession.hasError())
+      .isFalse();
+    assertThat(response.toLowerCase(Locale.US)).doesNotContain("error");
+  }
+
+  private void assertChangeMessages(String changeId, List<String> expected)
+      throws Exception {
+    ChangeInfo c = get(changeId);
+    Iterable<ChangeMessageInfo> messages = c.messages;
+    assertThat(messages).isNotNull();
+    assertThat(messages).hasSize(expected.size());
+    List<String> actual = new ArrayList<>();
+    for (ChangeMessageInfo info : messages) {
+      actual.add(info.message);
+    }
+    assertThat(actual).containsExactlyElementsIn(expected);
+  }
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
index 4e08f8d..053248f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/SubmitInput.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.extensions.api.changes;
 
 public class SubmitInput {
+  /** Not used anymore, kept for backward compatibility */
   @Deprecated
   public boolean waitForMerge;
+
   public String onBehalfOf;
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index f605abf..2b01b59 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -159,15 +159,6 @@
     final Grid formGrid = new Grid(12 + (flashClippy ? 1 : 0), 2);
 
     int row = 0;
-    formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, showSiteHeader);
-    row++;
-
-    if (flashClippy) {
-      formGrid.setText(row, labelIdx, "");
-      formGrid.setWidget(row, fieldIdx, useFlashClipboard);
-      row++;
-    }
 
     formGrid.setText(row, labelIdx, Util.C.reviewCategoryLabel());
     formGrid.setWidget(row, fieldIdx, reviewCategoryStrategy);
@@ -181,6 +172,18 @@
     formGrid.setWidget(row, fieldIdx, dateTimePanel);
     row++;
 
+    formGrid.setText(row, labelIdx, Util.C.emailFieldLabel());
+    formGrid.setWidget(row, fieldIdx, emailStrategy);
+    row++;
+
+    formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
+    formGrid.setWidget(row, fieldIdx, diffView);
+    row++;
+
+    formGrid.setText(row, labelIdx, "");
+    formGrid.setWidget(row, fieldIdx, showSiteHeader);
+    row++;
+
     formGrid.setText(row, labelIdx, "");
     formGrid.setWidget(row, fieldIdx, relativeDateInChangeTable);
     row++;
@@ -197,15 +200,14 @@
     formGrid.setWidget(row, fieldIdx, muteCommonPathPrefixes);
     row++;
 
-    formGrid.setText(row, labelIdx, Util.C.emailFieldLabel());
-    formGrid.setWidget(row, fieldIdx, emailStrategy);
-
     formGrid.setText(row, labelIdx, "");
     formGrid.setWidget(row, fieldIdx, signedOffBy);
     row++;
 
-    formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
-    formGrid.setWidget(row, fieldIdx, diffView);
+    if (flashClippy) {
+      formGrid.setText(row, labelIdx, "");
+      formGrid.setWidget(row, fieldIdx, useFlashClipboard);
+    }
 
     add(formGrid);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index e04509b..71942ce 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -142,6 +142,7 @@
     addReviewerIcon.setVisible(true);
     UIObject.setVisible(form, false);
     suggestBox.setFocus(false);
+    suggestBox.setText("");
   }
 
   private void addReviewer(final String reviewer, boolean confirmed) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index 8314e3e..bff3f47 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -192,8 +192,7 @@
 
   /** Submit a specific revision of a change. */
   public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
-    SubmitInput in = SubmitInput.create();
-    in.waitForMerge(true);
+    JavaScriptObject in = JavaScriptObject.createObject();
     call(id, commit, "submit").post(in, cb);
   }
 
@@ -287,17 +286,6 @@
     }
   }
 
-  private static class SubmitInput extends JavaScriptObject {
-    final native void waitForMerge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
-
-    static SubmitInput create() {
-      return (SubmitInput) createObject();
-    }
-
-    protected SubmitInput() {
-    }
-  }
-
   private static RestApi call(int id, String action) {
     return change(id).view(action);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
index 2996c07..33a2fb8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
@@ -637,17 +637,18 @@
       String contents,
       Element parent) {
     return CodeMirror.create(parent, Configuration.create()
-      .set("readOnly", true)
       .set("cursorBlinkRate", prefs.cursorBlinkRate())
       .set("cursorHeight", 0.85)
-      .set("lineNumbers", prefs.showLineNumbers())
-      .set("tabSize", prefs.tabSize())
-      .set("mode", fileSize == FileSize.SMALL ? getContentType(meta) : null)
-      .set("lineWrapping", false)
-      .set("scrollbarStyle", "overlay")
-      .set("styleSelectedText", true)
-      .set("showTrailingSpace", prefs.showWhitespaceErrors())
       .set("keyMap", "vim_ro")
+      .set("lineNumbers", prefs.showLineNumbers())
+      .set("lineWrapping", false)
+      .set("matchBrackets", prefs.matchBrackets())
+      .set("mode", fileSize == FileSize.SMALL ? getContentType(meta) : null)
+      .set("readOnly", true)
+      .set("scrollbarStyle", "overlay")
+      .set("showTrailingSpace", prefs.showWhitespaceErrors())
+      .set("styleSelectedText", true)
+      .set("tabSize", prefs.tabSize())
       .set("theme", prefs.theme().name().toLowerCase())
       .set("value", meta != null ? contents : "")
       .set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 3a3a33f..7d8d22c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -16,8 +16,8 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
-import com.google.gerrit.common.data.HostPageData;
 import com.google.common.base.Strings;
+import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.httpd.WebSessionManager.Key;
 import com.google.gerrit.httpd.WebSessionManager.Val;
 import com.google.gerrit.reviewdb.client.Account;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 5108315..1edf4a1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -20,6 +20,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.EventBroker;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.GerritOptions;
@@ -340,6 +341,7 @@
 
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
+    modules.add(new EventBroker.Module());
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(new DiffExecutorModule());
     modules.add(new MimeUtil2Module());
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 5bbb798..b586bc5 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
@@ -37,6 +37,7 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
@@ -51,11 +52,11 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
 import com.google.inject.Injector;
-import com.google.inject.Key;
-import com.google.inject.TypeLiteral;
 
 import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -85,6 +86,28 @@
   private Injector dbInjector;
   private Injector sysInjector;
 
+  @Inject
+  private AllUsersName allUsersName;
+
+  @Inject
+  private ChangeRebuilder rebuilder;
+
+  @Inject
+  @GerritServerConfig
+  private Config cfg;
+
+  @Inject
+  private GitRepositoryManager repoManager;
+
+  @Inject
+  private NotesMigration notesMigration;
+
+  @Inject
+  private SchemaFactory<ReviewDb> schemaFactory;
+
+  @Inject
+  private WorkQueue workQueue;
+
   @Override
   public int run() throws Exception {
     mustHaveValidSite();
@@ -96,8 +119,7 @@
     dbManager.start();
 
     sysInjector = createSysInjector();
-    NotesMigration notesMigration = sysInjector.getInstance(
-        NotesMigration.class);
+    sysInjector.injectMembers(this);
     if (!notesMigration.enabled()) {
       die("Notedb is not enabled.");
     }
@@ -107,16 +129,11 @@
 
     ListeningExecutorService executor = newExecutor();
     System.out.println("Rebuilding the notedb");
-    ChangeRebuilder rebuilder = sysInjector.getInstance(ChangeRebuilder.class);
 
     Multimap<Project.NameKey, Change.Id> changesByProject =
         getChangesByProject();
     AtomicBoolean ok = new AtomicBoolean(true);
     Stopwatch sw = Stopwatch.createStarted();
-    GitRepositoryManager repoManager =
-        sysInjector.getInstance(GitRepositoryManager.class);
-    Project.NameKey allUsersName =
-        sysInjector.getInstance(AllUsersName.class);
     try (Repository allUsersRepo =
         repoManager.openMetadataRepository(allUsersName)) {
       deleteRefs(RefNames.REFS_DRAFT_COMMENTS, allUsersRepo);
@@ -206,8 +223,7 @@
   private ListeningExecutorService newExecutor() {
     if (threads > 0) {
       return MoreExecutors.listeningDecorator(
-          dbInjector.getInstance(WorkQueue.class)
-            .createQueue(threads, "RebuildChange"));
+          workQueue.createQueue(threads, "RebuildChange"));
     } else {
       return MoreExecutors.newDirectExecutorService();
     }
@@ -217,8 +233,6 @@
       throws OrmException {
     // Memorize all changes so we can close the db connection and allow
     // rebuilder threads to use the full connection pool.
-    SchemaFactory<ReviewDb> schemaFactory = sysInjector.getInstance(Key.get(
-        new TypeLiteral<SchemaFactory<ReviewDb>>() {}));
     Multimap<Project.NameKey, Change.Id> changesByProject =
         ArrayListMultimap.create();
     try (ReviewDb db = schemaFactory.open()) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
index 6a3d7cb..7fdd7e2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.PluginData;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
@@ -30,6 +31,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.List;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
@@ -65,7 +67,11 @@
         result.add(new PluginData(pluginName, pluginVersion, tmpPlugin));
       }
     });
-    return result;
+    return FluentIterable.from(result).toSortedList(new Comparator<PluginData>() {
+      @Override
+      public int compare(PluginData a, PluginData b) {
+        return a.name.compareTo(b.name);
+      }});
   }
 
   private final ConsoleUI ui;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 400c1c0..3daeaaf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Account;
@@ -32,7 +33,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.AnonymousCowardName;
@@ -44,7 +44,6 @@
 import com.google.gerrit.server.data.PatchSetAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.gerrit.server.events.ChangeAbandonedEvent;
-import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.ChangeRestoredEvent;
 import com.google.gerrit.server.events.CommentAddedEvent;
@@ -54,17 +53,12 @@
 import com.google.gerrit.server.events.MergeFailedEvent;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.ProjectCreatedEvent;
-import com.google.gerrit.server.events.ProjectEvent;
-import com.google.gerrit.server.events.RefEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
 import com.google.gerrit.server.events.ReviewerAddedEvent;
 import com.google.gerrit.server.events.TopicChangedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -100,8 +94,8 @@
 
 /** Spawns local executables when a hook action occurs. */
 @Singleton
-public class ChangeHookRunner implements ChangeHooks, EventDispatcher,
-    LifecycleListener, NewProjectCreatedListener {
+public class ChangeHookRunner implements ChangeHooks, LifecycleListener,
+    NewProjectCreatedListener {
     /** A logger for this class. */
     private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
 
@@ -110,7 +104,6 @@
       protected void configure() {
         bind(ChangeHookRunner.class);
         bind(ChangeHooks.class).to(ChangeHookRunner.class);
-        bind(EventDispatcher.class).to(ChangeHookRunner.class);
         DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(ChangeHookRunner.class);
         listener().to(ChangeHookRunner.class);
       }
@@ -164,13 +157,6 @@
       }
     }
 
-    /** Listeners to receive changes as they happen (limited by visibility
-     *  of user). */
-    private final DynamicSet<UserScopedEventListener> listeners;
-
-    /** Listeners to receive all changes as they happen. */
-    private final DynamicSet<EventListener> unrestrictedListeners;
-
     /** Path of the new patchset hook. */
     private final Optional<Path> patchsetCreatedHook;
 
@@ -235,7 +221,7 @@
     /** Timeout value for synchronous hooks */
     private final int syncHookTimeout;
 
-    private final ChangeNotes.Factory notesFactory;
+    private DynamicItem<EventDispatcher> dispatcher;
 
     /**
      * Create a new ChangeHookRunner.
@@ -255,9 +241,7 @@
       ProjectCache projectCache,
       AccountCache accountCache,
       EventFactory eventFactory,
-      DynamicSet<UserScopedEventListener> listeners,
-      DynamicSet<EventListener> unrestrictedListeners,
-      ChangeNotes.Factory notesFactory) {
+      DynamicItem<EventDispatcher> dispatcher) {
         this.anonymousCowardName = anonymousCowardName;
         this.repoManager = repoManager;
         this.hookQueue = queue.createQueue(1, "hook");
@@ -265,9 +249,7 @@
         this.accountCache = accountCache;
         this.eventFactory = eventFactory;
         this.sitePaths = sitePath;
-        this.listeners = listeners;
-        this.unrestrictedListeners = unrestrictedListeners;
-        this.notesFactory = notesFactory;
+        this.dispatcher = dispatcher;
 
         Path hooksPath;
         String hooksPathConfig = config.getString("hooks", null, "path");
@@ -353,7 +335,7 @@
       event.projectName = project.get();
       event.headName = headName;
 
-      fireEvent(project, event);
+      dispatcher.get().postEvent(project, event);
 
       if (!projectCreatedHook.isPresent()) {
         return;
@@ -378,7 +360,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
       event.uploader = accountAttributeSupplier(uploader);
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!patchsetCreatedHook.isPresent()) {
         return;
@@ -415,7 +397,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
       event.uploader = accountAttributeSupplier(uploader);
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!draftPublishedHook.isPresent()) {
         return;
@@ -467,7 +449,7 @@
             }
           });
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!commentAddedHook.isPresent()) {
         return;
@@ -511,7 +493,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
       event.newRev = mergeResultRev;
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!changeMergedHook.isPresent()) {
         return;
@@ -546,7 +528,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
       event.reason = reason;
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!mergeFailedHook.isPresent()) {
         return;
@@ -581,7 +563,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
       event.reason = reason;
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!changeAbandonedHook.isPresent()) {
         return;
@@ -616,7 +598,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
       event.reason = reason;
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!changeRestoredHook.isPresent()) {
         return;
@@ -662,7 +644,7 @@
             }
           });
 
-      fireEvent(refName, event);
+      dispatcher.get().postEvent(refName, event);
 
       if (!refUpdatedHook.isPresent()) {
         return;
@@ -691,7 +673,7 @@
       event.patchSet = patchSetAttributeSupplier(change, patchSet);
       event.reviewer = accountAttributeSupplier(account);
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!reviewerAddedHook.isPresent()) {
         return;
@@ -721,7 +703,7 @@
       event.changer = accountAttributeSupplier(account);
       event.oldTopic = oldTopic;
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!topicChangedHook.isPresent()) {
         return;
@@ -762,7 +744,7 @@
       event.added = hashtagArray(added);
       event.removed = hashtagArray(removed);
 
-      fireEvent(change, event, db);
+      dispatcher.get().postEvent(change, event, db);
 
       if (!hashtagsChangedHook.isPresent()) {
         return;
@@ -810,28 +792,6 @@
       }
     }
 
-    @Override
-    public void postEvent(Change change, ChangeEvent event, ReviewDb db)
-        throws OrmException {
-      fireEvent(change, event, db);
-    }
-
-    @Override
-    public void postEvent(Branch.NameKey branchName, RefEvent event) {
-      fireEvent(branchName, event);
-    }
-
-    @Override
-    public void postEvent(Project.NameKey projectName, ProjectEvent event) {
-      fireEvent(projectName, event);
-    }
-
-    @Override
-    public void postEvent(com.google.gerrit.server.events.Event event,
-        ReviewDb db) throws OrmException {
-      fireEvent(event, db);
-    }
-
     private Supplier<AccountState> getAccountSupplier(
         final Account.Id account) {
       return Suppliers.memoize(
@@ -894,100 +854,6 @@
           });
     }
 
-    private void fireEventForUnrestrictedListeners(com.google.gerrit.server.events.Event event) {
-      for (EventListener listener : unrestrictedListeners) {
-        listener.onEvent(event);
-      }
-    }
-
-    private void fireEvent(Change change, ChangeEvent event, ReviewDb db)
-        throws OrmException {
-      for (UserScopedEventListener listener : listeners) {
-        if (isVisibleTo(change, listener.getUser(), db)) {
-          listener.onEvent(event);
-        }
-      }
-
-      fireEventForUnrestrictedListeners( event );
-    }
-
-    private void fireEvent(Project.NameKey project, ProjectEvent event) {
-      for (UserScopedEventListener listener : listeners) {
-        if (isVisibleTo(project, listener.getUser())) {
-          listener.onEvent(event);
-        }
-      }
-
-      fireEventForUnrestrictedListeners(event);
-    }
-
-    private void fireEvent(Branch.NameKey branchName, RefEvent event) {
-      for (UserScopedEventListener listener : listeners) {
-        if (isVisibleTo(branchName, listener.getUser())) {
-          listener.onEvent(event);
-        }
-      }
-
-      fireEventForUnrestrictedListeners(event);
-    }
-
-    private void fireEvent(com.google.gerrit.server.events.Event event,
-        ReviewDb db) throws OrmException {
-      for (UserScopedEventListener listener : listeners) {
-        if (isVisibleTo(event, listener.getUser(), db)) {
-          listener.onEvent(event);
-        }
-      }
-
-      fireEventForUnrestrictedListeners(event);
-    }
-
-    private boolean isVisibleTo(Project.NameKey project, CurrentUser user) {
-      ProjectState pe = projectCache.get(project);
-      if (pe == null) {
-        return false;
-      }
-      return pe.controlFor(user).isVisible();
-    }
-
-    private boolean isVisibleTo(Change change, CurrentUser user, ReviewDb db)
-        throws OrmException {
-      if (change == null) {
-        return false;
-      }
-      ProjectState pe = projectCache.get(change.getProject());
-      if (pe == null) {
-        return false;
-      }
-      ProjectControl pc = pe.controlFor(user);
-      return pc.controlFor(db, change).isVisible(db);
-    }
-
-    private boolean isVisibleTo(Branch.NameKey branchName, CurrentUser user) {
-      ProjectState pe = projectCache.get(branchName.getParentKey());
-      if (pe == null) {
-        return false;
-      }
-      ProjectControl pc = pe.controlFor(user);
-      return pc.controlForRef(branchName).isVisible();
-    }
-
-    private boolean isVisibleTo(com.google.gerrit.server.events.Event event,
-        CurrentUser user, ReviewDb db) throws OrmException {
-      if (event instanceof RefEvent) {
-        RefEvent refEvent = (RefEvent) event;
-        String ref = refEvent.getRefName();
-        if (PatchSet.isChangeRef(ref)) {
-          Change.Id cid = PatchSet.Id.fromRef(ref).getParentKey();
-          Change change = notesFactory
-              .create(db, refEvent.getProjectNameKey(), cid).getChange();
-          return isVisibleTo(change, user, db);
-        }
-        return isVisibleTo(refEvent.getBranchNameKey(), user);
-      }
-      return true;
-    }
-
     /**
      * Create an ApprovalAttribute for the given approval suitable for serialization to JSON.
      * @param approval
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
new file mode 100644
index 0000000..97bc2e5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
@@ -0,0 +1,185 @@
+// 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.common;
+
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.events.ChangeEvent;
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.ProjectEvent;
+import com.google.gerrit.server.events.RefEvent;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/** Distributes Events to listeners if they are allowed to see them */
+@Singleton
+public class EventBroker implements EventDispatcher {
+
+  public static class Module extends LifecycleModule {
+    @Override
+    protected void configure() {
+      DynamicItem.itemOf(binder(), EventDispatcher.class);
+      DynamicItem.bind(binder(), EventDispatcher.class).to(EventBroker.class);
+    }
+  }
+
+  /**
+   * Listeners to receive changes as they happen (limited by visibility of
+   * user).
+   */
+  protected final DynamicSet<UserScopedEventListener> listeners;
+
+  /** Listeners to receive all changes as they happen. */
+  protected final DynamicSet<EventListener> unrestrictedListeners;
+
+  protected final ProjectCache projectCache;
+
+  protected final ChangeNotes.Factory notesFactory;
+
+  @Inject
+  public EventBroker(DynamicSet<UserScopedEventListener> listeners,
+      DynamicSet<EventListener> unrestrictedListeners,
+      ProjectCache projectCache,
+      ChangeNotes.Factory notesFactory) {
+    this.listeners = listeners;
+    this.unrestrictedListeners = unrestrictedListeners;
+    this.projectCache = projectCache;
+    this.notesFactory = notesFactory;
+  }
+
+  @Override
+  public void postEvent(Change change, ChangeEvent event, ReviewDb db)
+      throws OrmException {
+    fireEvent(change, event, db);
+  }
+
+  @Override
+  public void postEvent(Branch.NameKey branchName, RefEvent event) {
+    fireEvent(branchName, event);
+  }
+
+  @Override
+  public void postEvent(Project.NameKey projectName, ProjectEvent event) {
+    fireEvent(projectName, event);
+  }
+
+  @Override
+  public void postEvent(Event event, ReviewDb db) throws OrmException {
+    fireEvent(event, db);
+  }
+
+  protected void fireEventForUnrestrictedListeners(Event event) {
+    for (EventListener listener : unrestrictedListeners) {
+      listener.onEvent(event);
+    }
+  }
+
+  protected void fireEvent(Change change, ChangeEvent event, ReviewDb db)
+      throws OrmException {
+    for (UserScopedEventListener listener : listeners) {
+      if (isVisibleTo(change, listener.getUser(), db)) {
+        listener.onEvent(event);
+      }
+    }
+    fireEventForUnrestrictedListeners(event);
+  }
+
+  protected void fireEvent(Project.NameKey project, ProjectEvent event) {
+    for (UserScopedEventListener listener : listeners) {
+      if (isVisibleTo(project, listener.getUser())) {
+        listener.onEvent(event);
+      }
+    }
+    fireEventForUnrestrictedListeners(event);
+  }
+
+  protected void fireEvent(Branch.NameKey branchName, RefEvent event) {
+    for (UserScopedEventListener listener : listeners) {
+      if (isVisibleTo(branchName, listener.getUser())) {
+        listener.onEvent(event);
+      }
+    }
+    fireEventForUnrestrictedListeners(event);
+  }
+
+  protected void fireEvent(Event event, ReviewDb db) throws OrmException {
+    for (UserScopedEventListener listener : listeners) {
+      if (isVisibleTo(event, listener.getUser(), db)) {
+        listener.onEvent(event);
+      }
+    }
+    fireEventForUnrestrictedListeners(event);
+  }
+
+  protected boolean isVisibleTo(Project.NameKey project, CurrentUser user) {
+    ProjectState pe = projectCache.get(project);
+    if (pe == null) {
+      return false;
+    }
+    return pe.controlFor(user).isVisible();
+  }
+
+  protected boolean isVisibleTo(Change change, CurrentUser user, ReviewDb db)
+      throws OrmException {
+    if (change == null) {
+      return false;
+    }
+    ProjectState pe = projectCache.get(change.getProject());
+    if (pe == null) {
+      return false;
+    }
+    ProjectControl pc = pe.controlFor(user);
+    return pc.controlFor(db, change).isVisible(db);
+  }
+
+  protected boolean isVisibleTo(Branch.NameKey branchName, CurrentUser user) {
+    ProjectState pe = projectCache.get(branchName.getParentKey());
+    if (pe == null) {
+      return false;
+    }
+    ProjectControl pc = pe.controlFor(user);
+    return pc.controlForRef(branchName).isVisible();
+  }
+
+  protected boolean isVisibleTo(Event event, CurrentUser user, ReviewDb db)
+      throws OrmException {
+    if (event instanceof RefEvent) {
+      RefEvent refEvent = (RefEvent) event;
+      String ref = refEvent.getRefName();
+      if (PatchSet.isChangeRef(ref)) {
+        Change.Id cid = PatchSet.Id.fromRef(ref).getParentKey();
+        Change change = notesFactory
+            .create(db, refEvent.getProjectNameKey(), cid).getChange();
+        return isVisibleTo(change, user, db);
+      }
+      return isVisibleTo(refEvent.getBranchNameKey(), user);
+    } else if (event instanceof ProjectEvent) {
+      return isVisibleTo(((ProjectEvent) event).getProjectNameKey(), user);
+    }
+    return true;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
new file mode 100644
index 0000000..e38f88c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -0,0 +1,286 @@
+// 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;
+
+import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GroupBaseInfo;
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.change.ReviewerSuggestionCache;
+import com.google.gerrit.server.change.SuggestReviewers;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ReviewersUtil {
+  private static final String MAX_SUFFIX = "\u9fa5";
+  private static final Ordering<SuggestedReviewerInfo> ORDERING =
+      Ordering.natural().onResultOf(new Function<SuggestedReviewerInfo, String>() {
+        @Nullable
+        @Override
+        public String apply(@Nullable SuggestedReviewerInfo suggestedReviewerInfo) {
+          if (suggestedReviewerInfo == null) {
+            return null;
+          }
+          return suggestedReviewerInfo.account != null
+              ? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
+              Strings.nullToEmpty(suggestedReviewerInfo.account.name))
+              : Strings.nullToEmpty(suggestedReviewerInfo.group.name);
+        }
+      });
+  private final AccountLoader accountLoader;
+  private final AccountCache accountCache;
+  private final ReviewerSuggestionCache reviewerSuggestionCache;
+  private final AccountControl accountControl;
+  private final Provider<ReviewDb> dbProvider;
+  private final GroupBackend groupBackend;
+  private final GroupMembers.Factory groupMembersFactory;
+  private final Provider<CurrentUser> currentUser;
+
+  @Inject
+  ReviewersUtil(AccountLoader.Factory accountLoaderFactory,
+      AccountCache accountCache,
+      ReviewerSuggestionCache reviewerSuggestionCache,
+      AccountControl.Factory accountControlFactory,
+      Provider<ReviewDb> dbProvider,
+      GroupBackend groupBackend,
+      GroupMembers.Factory groupMembersFactory,
+      Provider<CurrentUser> currentUser) {
+    this.accountLoader = accountLoaderFactory.create(true);
+    this.accountCache = accountCache;
+    this.reviewerSuggestionCache = reviewerSuggestionCache;
+    this.accountControl = accountControlFactory.get();
+    this.dbProvider = dbProvider;
+    this.groupBackend = groupBackend;
+    this.groupMembersFactory = groupMembersFactory;
+    this.currentUser = currentUser;
+  }
+
+  public interface VisibilityControl {
+    boolean isVisibleTo(Account.Id account) throws OrmException;
+  }
+
+  public List<SuggestedReviewerInfo> suggestReviewers(
+      SuggestReviewers suggestReviewers, ProjectControl projectControl,
+      VisibilityControl visibilityControl)
+      throws IOException, OrmException, BadRequestException {
+    String query = suggestReviewers.getQuery();
+    boolean suggestAccounts = suggestReviewers.getSuggestAccounts();
+    int suggestFrom = suggestReviewers.getSuggestFrom();
+    boolean useFullTextSearch = suggestReviewers.getUseFullTextSearch();
+    int limit = suggestReviewers.getLimit();
+
+    if (Strings.isNullOrEmpty(query)) {
+      throw new BadRequestException("missing query field");
+    }
+
+    if (!suggestAccounts || query.length() < suggestFrom) {
+      return Collections.emptyList();
+    }
+
+    List<AccountInfo> suggestedAccounts;
+    if (useFullTextSearch) {
+      suggestedAccounts = suggestAccountFullTextSearch(suggestReviewers, visibilityControl);
+    } else {
+      suggestedAccounts = suggestAccount(suggestReviewers, visibilityControl);
+    }
+
+    List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
+    for (AccountInfo a : suggestedAccounts) {
+      SuggestedReviewerInfo info = new SuggestedReviewerInfo();
+      info.account = a;
+      reviewer.add(info);
+    }
+
+    for (GroupReference g : suggestAccountGroup(suggestReviewers, projectControl)) {
+      if (suggestGroupAsReviewer(suggestReviewers, projectControl.getProject(),
+          g, visibilityControl)) {
+        GroupBaseInfo info = new GroupBaseInfo();
+        info.id = Url.encode(g.getUUID().get());
+        info.name = g.getName();
+        SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
+        suggestedReviewerInfo.group = info;
+        reviewer.add(suggestedReviewerInfo);
+      }
+    }
+
+    reviewer = ORDERING.immutableSortedCopy(reviewer);
+    if (reviewer.size() <= limit) {
+      return reviewer;
+    } else {
+      return reviewer.subList(0, limit);
+    }
+  }
+
+  private List<AccountInfo> suggestAccountFullTextSearch(
+      SuggestReviewers suggestReviewers, VisibilityControl visibilityControl)
+          throws IOException, OrmException {
+    List<AccountInfo> results = reviewerSuggestionCache.search(
+        suggestReviewers.getQuery(), suggestReviewers.getFullTextMaxMatches());
+
+    Iterator<AccountInfo> it = results.iterator();
+    while (it.hasNext()) {
+      Account.Id accountId = new Account.Id(it.next()._accountId);
+      if (!(visibilityControl.isVisibleTo(accountId)
+          && accountControl.canSee(accountId))) {
+        it.remove();
+      }
+    }
+
+    return results;
+  }
+
+  private List<AccountInfo> suggestAccount(SuggestReviewers suggestReviewers,
+      VisibilityControl visibilityControl)
+      throws OrmException {
+    String query = suggestReviewers.getQuery();
+    int limit = suggestReviewers.getLimit();
+
+    String a = query;
+    String b = a + MAX_SUFFIX;
+
+    Map<Account.Id, AccountInfo> r = new LinkedHashMap<>();
+    Map<Account.Id, String> queryEmail = new HashMap<>();
+
+    for (Account p : dbProvider.get().accounts()
+        .suggestByFullName(a, b, limit)) {
+      if (p.isActive()) {
+        addSuggestion(r, p.getId(), visibilityControl);
+      }
+    }
+
+    if (r.size() < limit) {
+      for (Account p : dbProvider.get().accounts()
+          .suggestByPreferredEmail(a, b, limit - r.size())) {
+        if (p.isActive()) {
+          addSuggestion(r, p.getId(), visibilityControl);
+        }
+      }
+    }
+
+    if (r.size() < limit) {
+      for (AccountExternalId e : dbProvider.get().accountExternalIds()
+          .suggestByEmailAddress(a, b, limit - r.size())) {
+        if (!r.containsKey(e.getAccountId())) {
+          Account p = accountCache.get(e.getAccountId()).getAccount();
+          if (p.isActive()) {
+            if (addSuggestion(r, p.getId(), visibilityControl)) {
+              queryEmail.put(e.getAccountId(), e.getEmailAddress());
+            }
+          }
+        }
+      }
+    }
+
+    accountLoader.fill();
+    for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
+      AccountInfo info = r.get(p.getKey());
+      if (info != null) {
+        info.email = p.getValue();
+      }
+    }
+    return new ArrayList<>(r.values());
+  }
+
+  private boolean addSuggestion(Map<Account.Id, AccountInfo> map,
+      Account.Id account, VisibilityControl visibilityControl)
+      throws OrmException {
+    if (!map.containsKey(account)
+        // Can the suggestion see the change?
+        && visibilityControl.isVisibleTo(account)
+        // Can the account see the current user?
+        && accountControl.canSee(account)) {
+      map.put(account, accountLoader.get(account));
+      return true;
+    }
+    return false;
+  }
+
+  private List<GroupReference> suggestAccountGroup(
+      SuggestReviewers suggestReviewers, ProjectControl ctl) {
+    return Lists.newArrayList(
+        Iterables.limit(groupBackend.suggest(suggestReviewers.getQuery(), ctl),
+            suggestReviewers.getLimit()));
+  }
+
+  private boolean suggestGroupAsReviewer(SuggestReviewers suggestReviewers,
+      Project project, GroupReference group,
+      VisibilityControl visibilityControl) throws OrmException, IOException {
+    int maxAllowed = suggestReviewers.getMaxAllowed();
+
+    if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
+      return false;
+    }
+
+    try {
+      Set<Account> members = groupMembersFactory
+          .create(currentUser.get())
+          .listAccounts(group.getUUID(), project.getNameKey());
+
+      if (members.isEmpty()) {
+        return false;
+      }
+
+      if (maxAllowed > 0 && members.size() > maxAllowed) {
+        return false;
+      }
+
+      // require that at least one member in the group can see the change
+      for (Account account : members) {
+        if (visibilityControl.isVisibleTo(account.getId())) {
+          return true;
+        }
+      }
+    } catch (NoSuchGroupException e) {
+      return false;
+    } catch (NoSuchProjectException e) {
+      return false;
+    }
+
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 446c8ed..a9bf220 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -52,7 +52,7 @@
 import com.google.gerrit.server.change.Reviewers;
 import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.change.SubmittedTogether;
-import com.google.gerrit.server.change.SuggestReviewers;
+import com.google.gerrit.server.change.SuggestChangeReviewers;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
@@ -77,7 +77,7 @@
   private final Revisions revisions;
   private final ReviewerApiImpl.Factory reviewerApi;
   private final RevisionApiImpl.Factory revisionApi;
-  private final Provider<SuggestReviewers> suggestReviewers;
+  private final Provider<SuggestChangeReviewers> suggestReviewers;
   private final ChangeResource change;
   private final Abandon abandon;
   private final Revert revert;
@@ -104,7 +104,7 @@
       Revisions revisions,
       ReviewerApiImpl.Factory reviewerApi,
       RevisionApiImpl.Factory revisionApi,
-      Provider<SuggestReviewers> suggestReviewers,
+      Provider<SuggestChangeReviewers> suggestReviewers,
       Abandon abandon,
       Revert revert,
       Restore restore,
@@ -304,7 +304,7 @@
   private List<SuggestedReviewerInfo> suggestReviewers(SuggestedReviewersRequest r)
       throws RestApiException {
     try {
-      SuggestReviewers mySuggestReviewers = suggestReviewers.get();
+      SuggestChangeReviewers mySuggestReviewers = suggestReviewers.get();
       mySuggestReviewers.setQuery(r.getQuery());
       mySuggestReviewers.setLimit(r.getLimit());
       return mySuggestReviewers.apply(change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 0ef8b51..37f15bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -72,7 +72,7 @@
     post(CHANGE_KIND, "index").to(Index.class);
 
     post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
-    get(CHANGE_KIND, "suggest_reviewers").to(SuggestReviewers.class);
+    get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
     child(CHANGE_KIND, "reviewers").to(Reviewers.class);
     get(REVIEWER_KIND).to(GetReviewer.class);
     delete(REVIEWER_KIND).to(DeleteReviewer.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
index dd9a0b9..ae2672b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -22,9 +22,9 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
index ffb63f6..20078cc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
@@ -102,7 +102,7 @@
             });
   }
 
-  List<AccountInfo> search(String query, int n) throws IOException {
+  public List<AccountInfo> search(String query, int n) throws IOException {
     IndexSearcher searcher = get();
     if (searcher == null) {
       return Collections.emptyList();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 4e96704..fb69f87 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -15,11 +15,14 @@
 package com.google.gerrit.server.change;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.ParameterizedString;
@@ -71,6 +74,7 @@
 import java.io.IOException;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -96,7 +100,7 @@
   private static final String CLICK_FAILURE_TOOLTIP =
       "Clicking the button would fail";
   private static final String CHANGES_NOT_MERGEABLE =
-      "See the \"Submitted Together\" tab for problems";
+      "See the \"Submitted Together\" tab for problems, specially see: ";
 
   public static class Output {
     transient Change change;
@@ -114,10 +118,8 @@
   public static class TestSubmitInput extends SubmitInput {
     public final boolean failAfterRefUpdates;
 
-    @SuppressWarnings("deprecation")
     public TestSubmitInput(SubmitInput base, boolean failAfterRefUpdates) {
       this.onBehalfOf = base.onBehalfOf;
-      this.waitForMerge = base.waitForMerge;
       this.failAfterRefUpdates = failAfterRefUpdates;
     }
   }
@@ -263,11 +265,18 @@
         MergeOp.checkSubmitRule(c);
       }
 
-      Boolean csIsMergeable = isPatchSetMergeable(cs);
-      if (csIsMergeable == null) {
+      Collection<ChangeData> unmergeable = unmergeableChanges(cs);
+      if (unmergeable == null) {
         return CLICK_FAILURE_TOOLTIP;
-      } else if (!csIsMergeable) {
-        return CHANGES_NOT_MERGEABLE;
+      } else if (!unmergeable.isEmpty()) {
+        return CHANGES_NOT_MERGEABLE + Joiner.on(", ").join(
+            Iterables.transform(unmergeable,
+                new Function<ChangeData, String>() {
+              @Override
+              public String apply(ChangeData cd) {
+                return String.valueOf(cd.getId().get());
+              }
+            }));
       }
     } catch (ResourceConflictException e) {
       return BLOCKED_SUBMIT_TOOLTIP;
@@ -407,11 +416,11 @@
     return change != null ? change.getStatus().name().toLowerCase() : "deleted";
   }
 
-  public Boolean isPatchSetMergeable(ChangeSet cs)
+  public Collection<ChangeData> unmergeableChanges(ChangeSet cs)
       throws OrmException, IOException {
-    Map<ChangeData, Boolean> mergeabilityMap = new HashMap<>();
+    Set<ChangeData> mergeabilityMap = new HashSet<>();
     for (ChangeData change : cs.changes()) {
-      mergeabilityMap.put(change, false);
+      mergeabilityMap.add(change);
     }
 
     Multimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch();
@@ -442,17 +451,19 @@
           // Skip whole check, cannot determine if mergeable
           return null;
         }
-        mergeabilityMap.put(change, mergeable);
+        if (mergeable) {
+          mergeabilityMap.remove(change);
+        }
 
         if (isLastInChain && isMergeCommit && mergeable) {
           for (ChangeData c : targetBranch) {
-            mergeabilityMap.put(c, true);
+            mergeabilityMap.remove(c);
           }
           break;
         }
       }
     }
-    return !mergeabilityMap.values().contains(Boolean.FALSE);
+    return mergeabilityMap;
   }
 
   private HashMap<Change.Id, RevCommit> findCommits(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
new file mode 100644
index 0000000..46fbe67
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -0,0 +1,76 @@
+// 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.change;
+
+import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.ReviewersUtil;
+import com.google.gerrit.server.ReviewersUtil.VisibilityControl;
+import com.google.gerrit.server.account.AccountVisibility;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SuggestChangeReviewers extends SuggestReviewers
+    implements RestReadView<ChangeResource> {
+  @Inject
+  SuggestChangeReviewers(AccountVisibility av,
+      GenericFactory identifiedUserFactory,
+      Provider<ReviewDb> dbProvider,
+      @GerritServerConfig Config cfg,
+      ReviewersUtil reviewersUtil) {
+    super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil);
+  }
+
+  @Override
+  public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
+      throws BadRequestException, OrmException, IOException {
+    return reviewersUtil.suggestReviewers(this,
+        rsrc.getControl().getProjectControl(), getVisibility(rsrc));
+  }
+
+  private VisibilityControl getVisibility(final ChangeResource rsrc) {
+    if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
+      return new VisibilityControl() {
+        @Override
+        public boolean isVisibleTo(Account.Id account) throws OrmException {
+          return true;
+        }
+      };
+    } else {
+      return new VisibilityControl() {
+        @Override
+        public boolean isVisibleTo(Account.Id account) throws OrmException {
+          IdentifiedUser who =
+              identifiedUserFactory.create(dbProvider, account);
+          // we can't use changeControl directly as it won't suggest reviewers
+          // to drafts
+          return rsrc.getControl().forUser(who).isRefVisible();
+        }
+      };
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
index 4561ae4..3b61033 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -14,89 +14,33 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Function;
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.common.GroupBaseInfo;
-import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.extensions.restapi.Url;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountControl;
-import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.ReviewersUtil;
 import com.google.gerrit.server.account.AccountVisibility;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class SuggestReviewers implements RestReadView<ChangeResource> {
-  private static final String MAX_SUFFIX = "\u9fa5";
+public class SuggestReviewers {
   private static final int DEFAULT_MAX_SUGGESTED = 10;
   private static final int DEFAULT_MAX_MATCHES = 100;
-  private static final Ordering<SuggestedReviewerInfo> ORDERING =
-      Ordering.natural().onResultOf(new Function<SuggestedReviewerInfo, String>() {
-        @Nullable
-        @Override
-        public String apply(@Nullable SuggestedReviewerInfo suggestedReviewerInfo) {
-          if (suggestedReviewerInfo == null) {
-            return null;
-          }
-          return suggestedReviewerInfo.account != null
-              ? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
-              Strings.nullToEmpty(suggestedReviewerInfo.account.name))
-              : Strings.nullToEmpty(suggestedReviewerInfo.group.name);
-        }
-      });
 
-  private final AccountLoader accountLoader;
-  private final AccountControl accountControl;
-  private final GroupMembers.Factory groupMembersFactory;
-  private final AccountCache accountCache;
-  private final Provider<ReviewDb> dbProvider;
-  private final Provider<CurrentUser> currentUser;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final GroupBackend groupBackend;
+  protected final Provider<ReviewDb> dbProvider;
+  protected final IdentifiedUser.GenericFactory identifiedUserFactory;
+  protected final ReviewersUtil reviewersUtil;
+
   private final boolean suggestAccounts;
   private final int suggestFrom;
   private final int maxAllowed;
-  private int limit;
-  private String query;
+  protected int limit;
+  protected String query;
   private boolean useFullTextSearch;
   private final int fullTextMaxMatches;
-  private final int maxSuggestedReviewers;
-  private final ReviewerSuggestionCache reviewerSuggestionCache;
+  protected final int maxSuggestedReviewers;
 
   @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
       usage = "maximum number of reviewers to list")
@@ -112,27 +56,43 @@
     this.query = q;
   }
 
+  public String getQuery() {
+    return query;
+  }
+
+  public boolean getSuggestAccounts() {
+    return suggestAccounts;
+  }
+
+  public int getSuggestFrom() {
+    return suggestFrom;
+  }
+
+  public boolean getUseFullTextSearch() {
+    return useFullTextSearch;
+  }
+
+  public int getFullTextMaxMatches() {
+    return fullTextMaxMatches;
+  }
+
+  public int getLimit() {
+    return limit;
+  }
+
+  public int getMaxAllowed() {
+    return maxAllowed;
+  }
+
   @Inject
-  SuggestReviewers(AccountVisibility av,
-      AccountLoader.Factory accountLoaderFactory,
-      AccountControl.Factory accountControlFactory,
-      AccountCache accountCache,
-      GroupMembers.Factory groupMembersFactory,
+  public SuggestReviewers(AccountVisibility av,
       IdentifiedUser.GenericFactory identifiedUserFactory,
-      Provider<CurrentUser> currentUser,
       Provider<ReviewDb> dbProvider,
       @GerritServerConfig Config cfg,
-      GroupBackend groupBackend,
-      ReviewerSuggestionCache reviewerSuggestionCache) {
-    this.accountLoader = accountLoaderFactory.create(true);
-    this.accountControl = accountControlFactory.get();
-    this.accountCache = accountCache;
-    this.groupMembersFactory = groupMembersFactory;
+      ReviewersUtil reviewersUtil) {
     this.dbProvider = dbProvider;
     this.identifiedUserFactory = identifiedUserFactory;
-    this.currentUser = currentUser;
-    this.groupBackend = groupBackend;
-    this.reviewerSuggestionCache = reviewerSuggestionCache;
+    this.reviewersUtil = reviewersUtil;
     this.maxSuggestedReviewers =
         cfg.getInt("suggest", "maxSuggestedReviewers", DEFAULT_MAX_SUGGESTED);
     this.limit = this.maxSuggestedReviewers;
@@ -152,196 +112,4 @@
     this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed",
         PostReviewers.DEFAULT_MAX_REVIEWERS);
   }
-
-  private interface VisibilityControl {
-    boolean isVisibleTo(Account.Id account) throws OrmException;
-  }
-
-  @Override
-  public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
-      throws BadRequestException, OrmException, IOException {
-    if (Strings.isNullOrEmpty(query)) {
-      throw new BadRequestException("missing query field");
-    }
-
-    if (!suggestAccounts || query.length() < suggestFrom) {
-      return Collections.emptyList();
-    }
-
-    VisibilityControl visibilityControl = getVisibility(rsrc);
-    List<AccountInfo> suggestedAccounts;
-    if (useFullTextSearch) {
-      suggestedAccounts = suggestAccountFullTextSearch(visibilityControl);
-    } else {
-      suggestedAccounts = suggestAccount(visibilityControl);
-    }
-
-    List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
-    for (AccountInfo a : suggestedAccounts) {
-      SuggestedReviewerInfo info = new SuggestedReviewerInfo();
-      info.account = a;
-      reviewer.add(info);
-    }
-
-    Project p = rsrc.getControl().getProject();
-    for (GroupReference g : suggestAccountGroup(
-        rsrc.getControl().getProjectControl())) {
-      if (suggestGroupAsReviewer(p, g, visibilityControl)) {
-        GroupBaseInfo info = new GroupBaseInfo();
-        info.id = Url.encode(g.getUUID().get());
-        info.name = g.getName();
-        SuggestedReviewerInfo suggestedReviewerInfo = new SuggestedReviewerInfo();
-        suggestedReviewerInfo.group = info;
-        reviewer.add(suggestedReviewerInfo);
-      }
-    }
-
-    reviewer = ORDERING.immutableSortedCopy(reviewer);
-    if (reviewer.size() <= limit) {
-      return reviewer;
-    } else {
-      return reviewer.subList(0, limit);
-    }
-  }
-
-  private VisibilityControl getVisibility(final ChangeResource rsrc) {
-    if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
-      return new VisibilityControl() {
-        @Override
-        public boolean isVisibleTo(Account.Id account) throws OrmException {
-          return true;
-        }
-      };
-    } else {
-      return new VisibilityControl() {
-        @Override
-        public boolean isVisibleTo(Account.Id account) throws OrmException {
-          IdentifiedUser who =
-              identifiedUserFactory.create(dbProvider, account);
-          // we can't use changeControl directly as it won't suggest reviewers
-          // to drafts
-          return rsrc.getControl().forUser(who).isRefVisible();
-        }
-      };
-    }
-  }
-
-  private List<GroupReference> suggestAccountGroup(ProjectControl ctl) {
-    return Lists.newArrayList(
-        Iterables.limit(groupBackend.suggest(query, ctl), limit));
-  }
-
-  private List<AccountInfo> suggestAccount(VisibilityControl visibilityControl)
-      throws OrmException {
-    String a = query;
-    String b = a + MAX_SUFFIX;
-
-    Map<Account.Id, AccountInfo> r = new LinkedHashMap<>();
-    Map<Account.Id, String> queryEmail = new HashMap<>();
-
-    for (Account p : dbProvider.get().accounts()
-        .suggestByFullName(a, b, limit)) {
-      if (p.isActive()) {
-        addSuggestion(r, p.getId(), visibilityControl);
-      }
-    }
-
-    if (r.size() < limit) {
-      for (Account p : dbProvider.get().accounts()
-          .suggestByPreferredEmail(a, b, limit - r.size())) {
-        if (p.isActive()) {
-          addSuggestion(r, p.getId(), visibilityControl);
-        }
-      }
-    }
-
-    if (r.size() < limit) {
-      for (AccountExternalId e : dbProvider.get().accountExternalIds()
-          .suggestByEmailAddress(a, b, limit - r.size())) {
-        if (!r.containsKey(e.getAccountId())) {
-          Account p = accountCache.get(e.getAccountId()).getAccount();
-          if (p.isActive()) {
-            if (addSuggestion(r, p.getId(), visibilityControl)) {
-              queryEmail.put(e.getAccountId(), e.getEmailAddress());
-            }
-          }
-        }
-      }
-    }
-
-    accountLoader.fill();
-    for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
-      AccountInfo info = r.get(p.getKey());
-      if (info != null) {
-        info.email = p.getValue();
-      }
-    }
-    return new ArrayList<>(r.values());
-  }
-
-  private List<AccountInfo> suggestAccountFullTextSearch(
-      VisibilityControl visibilityControl) throws IOException, OrmException {
-    List<AccountInfo> results = reviewerSuggestionCache.search(
-        query, fullTextMaxMatches);
-
-    Iterator<AccountInfo> it = results.iterator();
-    while (it.hasNext()) {
-      Account.Id accountId = new Account.Id(it.next()._accountId);
-      if (!(visibilityControl.isVisibleTo(accountId)
-          && accountControl.canSee(accountId))) {
-        it.remove();
-      }
-    }
-
-    return results;
-  }
-
-  private boolean addSuggestion(Map<Account.Id, AccountInfo> map,
-      Account.Id account, VisibilityControl visibilityControl)
-      throws OrmException {
-    if (!map.containsKey(account)
-        // Can the suggestion see the change?
-        && visibilityControl.isVisibleTo(account)
-        // Can the account see the current user?
-        && accountControl.canSee(account)) {
-      map.put(account, accountLoader.get(account));
-      return true;
-    }
-    return false;
-  }
-
-  private boolean suggestGroupAsReviewer(Project project,
-      GroupReference group, VisibilityControl visibilityControl)
-      throws OrmException, IOException {
-    if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
-      return false;
-    }
-
-    try {
-      Set<Account> members = groupMembersFactory
-          .create(currentUser.get())
-          .listAccounts(group.getUUID(), project.getNameKey());
-
-      if (members.isEmpty()) {
-        return false;
-      }
-
-      if (maxAllowed > 0 && members.size() > maxAllowed) {
-        return false;
-      }
-
-      // require that at least one member in the group can see the change
-      for (Account account : members) {
-        if (visibilityControl.isVisibleTo(account.getId())) {
-          return true;
-        }
-      }
-    } catch (NoSuchGroupException e) {
-      return false;
-    } catch (NoSuchProjectException e) {
-      return false;
-    }
-
-    return false;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 7d9bd15..dee43ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -79,6 +79,7 @@
 import com.google.gerrit.server.change.MergeabilityCacheImpl;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.events.EventsMetrics;
+import com.google.gerrit.server.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.EmailMerge;
@@ -272,6 +273,7 @@
     DynamicSet.setOf(binder(), ReceivePackInitializer.class);
     DynamicSet.setOf(binder(), PostReceiveHook.class);
     DynamicSet.setOf(binder(), PreUploadHook.class);
+    DynamicSet.setOf(binder(), ChangeIndexedListener.class);
     DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
     DynamicSet.setOf(binder(), ProjectDeletedListener.class);
     DynamicSet.setOf(binder(), GarbageCollectorListener.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeIndexedListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeIndexedListener.java
new file mode 100644
index 0000000..f996724
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeIndexedListener.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2015 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.query.change.ChangeData;
+
+/** Notified whenever a change is indexed or deleted from the index. */
+@ExtensionPoint
+public interface ChangeIndexedListener {
+  /** Invoked when a change is indexed. */
+  void onChangeIndexed(ChangeData change);
+
+  /** Invoked when a change is deleted from the index. */
+  void onChangeDeleted(Change.Id id);
+}
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 72517dc..775ecae 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
@@ -1775,6 +1775,7 @@
             .setExtraCC(recipients.getCcOnly())
             .setApprovals(approvals)
             .setMessage(msg)
+            .setNotify(magicBranch.notify)
             .setRequestScopePropagator(requestScopePropagator)
             .setSendMail(true)
             .setUpdateRef(true));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index bdf9f052..b32d668 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -361,8 +361,8 @@
     PatchSet.Id psId = update.getPatchSetId();
     ctx.getDb().patchSetApprovals().upsert(
         convertPatchSet(normalized.getNormalized(), psId));
-    ctx.getDb().patchSetApprovals().delete(
-        convertPatchSet(normalized.deleted(), psId));
+    ctx.getDb().patchSetApprovals().update(
+        zero(convertPatchSet(normalized.deleted(), psId)));
     for (PatchSetApproval psa : normalized.updated()) {
       update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
     }
@@ -400,6 +400,19 @@
     return Iterables.transform(approvals, convertPatchSet(psId));
   }
 
+  private static Iterable<PatchSetApproval> zero(
+      Iterable<PatchSetApproval> approvals) {
+    return Iterables.transform(approvals,
+        new Function<PatchSetApproval, PatchSetApproval>() {
+          @Override
+          public PatchSetApproval apply(PatchSetApproval in) {
+            PatchSetApproval copy = new PatchSetApproval(in.getPatchSetId(), in);
+            copy.setValue((short) 0);
+            return copy;
+          }
+        });
+  }
+
   private String getByAccountName() {
     checkNotNull(submitter,
         "getByAccountName called before submitter populated");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
index a0860c7..0f9b459 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -20,10 +20,12 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.extensions.registration.DynamicSet;
 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.CurrentUser;
+import com.google.gerrit.server.extensions.events.ChangeIndexedListener;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -94,11 +96,13 @@
   private final ChangeData.Factory changeDataFactory;
   private final ThreadLocalRequestContext context;
   private final ListeningExecutorService executor;
+  private final DynamicSet<ChangeIndexedListener> indexedListener;
 
   @AssistedInject
   ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
+      DynamicSet<ChangeIndexedListener> indexedListener,
       @Assisted ListeningExecutorService executor,
       @Assisted ChangeIndex index) {
     this.executor = executor;
@@ -107,12 +111,14 @@
     this.context = context;
     this.index = index;
     this.indexes = null;
+    this.indexedListener = indexedListener;
   }
 
   @AssistedInject
   ChangeIndexer(SchemaFactory<ReviewDb> schemaFactory,
       ChangeData.Factory changeDataFactory,
       ThreadLocalRequestContext context,
+      DynamicSet<ChangeIndexedListener> indexedListener,
       @Assisted ListeningExecutorService executor,
       @Assisted IndexCollection indexes) {
     this.executor = executor;
@@ -121,6 +127,7 @@
     this.context = context;
     this.index = null;
     this.indexes = indexes;
+    this.indexedListener = indexedListener;
   }
 
   /**
@@ -160,6 +167,19 @@
     for (ChangeIndex i : getWriteIndexes()) {
       i.replace(cd);
     }
+    fireChangeIndexedEvent(cd);
+  }
+
+  private void fireChangeIndexedEvent(ChangeData change) {
+    for (ChangeIndexedListener listener : indexedListener) {
+      listener.onChangeIndexed(change);
+    }
+  }
+
+  private void fireChangeDeletedFromIndexEvent(Change.Id id) {
+    for (ChangeIndexedListener listener : indexedListener) {
+      listener.onChangeDeleted(id);
+    }
   }
 
   /**
@@ -280,6 +300,7 @@
       for (ChangeIndex i : getWriteIndexes()) {
         i.delete(id);
       }
+      fireChangeDeletedFromIndexEvent(id);
       return null;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index da1e7b5..19a0535 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -97,14 +97,13 @@
   public PatchList get(Change change, PatchSet patchSet)
       throws PatchListNotAvailableException {
     Project.NameKey project = change.getProject();
-    ObjectId a = null;
     if (patchSet.getRevision() == null) {
       throw new PatchListNotAvailableException(
           "revision is null for " + patchSet.getId());
     }
     ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
     Whitespace ws = Whitespace.IGNORE_NONE;
-    return get(new PatchListKey(a, b, ws), project);
+    return get(new PatchListKey(null, b, ws), project);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
index 1d717ef..0146f84 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -40,10 +40,6 @@
 public class ListPlugins implements RestReadView<TopLevelResource> {
   private final PluginLoader pluginLoader;
 
-  @Deprecated
-  @Option(name = "--format", usage = "(deprecated) output format")
-  private OutputFormat format = OutputFormat.TEXT;
-
   @Option(name = "--all", aliases = {"-a"}, usage = "List all plugins, including disabled plugins")
   private boolean all;
 
@@ -52,23 +48,12 @@
     this.pluginLoader = pluginLoader;
   }
 
-  public OutputFormat getFormat() {
-    return format;
-  }
-
-  public ListPlugins setFormat(OutputFormat fmt) {
-    this.format = fmt;
-    return this;
-  }
-
   @Override
   public Object apply(TopLevelResource resource) {
-    format = OutputFormat.JSON;
     return display(null);
   }
 
   public JsonElement display(PrintWriter stdout) {
-    Map<String, PluginInfo> output = Maps.newTreeMap();
     List<Plugin> plugins = Lists.newArrayList(pluginLoader.getPlugins(all));
     Collections.sort(plugins, new Comparator<Plugin>() {
       @Override
@@ -77,33 +62,28 @@
       }
     });
 
-    if (!format.isJson()) {
+    if (stdout == null) {
+      Map<String, PluginInfo> output = Maps.newTreeMap();
+      for (Plugin p : plugins) {
+        PluginInfo info = new PluginInfo(p);
+        output.put(p.getName(), info);
+      }
+      return OutputFormat.JSON.newGson().toJsonTree(
+          output,
+          new TypeToken<Map<String, Object>>() {}.getType());
+    } else {
       stdout.format("%-30s %-10s %-8s %s\n", "Name", "Version", "Status", "File");
       stdout.print("-------------------------------------------------------------------------------\n");
-    }
-
-    for (Plugin p : plugins) {
-      PluginInfo info = new PluginInfo(p);
-      if (format.isJson()) {
-        output.put(p.getName(), info);
-      } else {
+      for (Plugin p : plugins) {
+        PluginInfo info = new PluginInfo(p);
         stdout.format("%-30s %-10s %-8s %s\n", p.getName(),
             Strings.nullToEmpty(info.version),
             p.isDisabled() ? "DISABLED" : "ENABLED",
             p.getSrcFile().getFileName());
+        stdout.print('\n');
       }
+      stdout.flush();
     }
-
-    if (stdout == null) {
-      return OutputFormat.JSON.newGson().toJsonTree(
-          output,
-          new TypeToken<Map<String, Object>>() {}.getType());
-    } else if (format.isJson()) {
-      format.newGson().toJson(output,
-          new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
-      stdout.print('\n');
-    }
-    stdout.flush();
     return null;
   }
 
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 312ac89..53a7c3b 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
@@ -96,8 +96,8 @@
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 @Ignore
 public abstract class AbstractQueryChangesTest extends GerritServerTests {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
index 7adf721..f2d563e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
@@ -97,6 +97,13 @@
     messages.add(Message.create(from, rcpt, headers, body));
   }
 
+  public void clear() {
+    waitForEmails();
+    synchronized (messages) {
+      messages.clear();
+    }
+  }
+
   public ImmutableList<Message> getMessages() {
     waitForEmails();
     synchronized (messages) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index cec3249..fed8226 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -157,6 +157,9 @@
   }
 
   private Multimap<String, ?> extractParameters(DispatchCommand dcmd) {
+    if (dcmd == null) {
+      return ArrayListMultimap.create(0, 0);
+    }
     String[] cmdArgs = dcmd.getArguments();
     String paramName = null;
     int argPos = 0;
@@ -274,6 +277,9 @@
   }
 
   private String extractWhat(DispatchCommand dcmd) {
+    if (dcmd == null) {
+      return "Command was already destroyed";
+    }
     StringBuilder commandName = new StringBuilder(dcmd.getCommandName());
     String[] args = dcmd.getArguments();
     for (int i = 1; i < args.length; i++) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index ce969da..00cf53f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -262,9 +262,6 @@
   }
 
   private void reviewPatchSet(final PatchSet patchSet) throws Exception {
-    if (changeComment == null) {
-      changeComment = "";
-    }
     if (notify == null) {
       notify = NotifyHandling.ALL;
     }
@@ -283,22 +280,20 @@
     }
     review.labels.putAll(customLabels);
 
-    // If review labels are being applied, the comment will be included
-    // on the review note. We don't need to add it again on the abandon
-    // or restore comment.
-    if (!review.labels.isEmpty() && (abandonChange || restoreChange)) {
-      changeComment = null;
+    // We don't need to add the review comment when abandoning/restoring.
+    if (abandonChange || restoreChange) {
+      review.message = null;
     }
 
     try {
       if (abandonChange) {
         AbandonInput input = new AbandonInput();
-        input.message = changeComment;
+        input.message = Strings.emptyToNull(changeComment);
         applyReview(patchSet, review);
         changeApi(patchSet).abandon(input);
       } else if (restoreChange) {
         RestoreInput input = new RestoreInput();
-        input.message = changeComment;
+        input.message = Strings.emptyToNull(changeComment);
         changeApi(patchSet).restore(input);
         applyReview(patchSet, review);
       } else {
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index cf3e76c..20c3c2a 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.EventBroker;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.auth.oauth.OAuthModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
@@ -294,6 +295,7 @@
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<>();
     modules.add(new DropWizardMetricMaker.RestModule());
+    modules.add(new EventBroker.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(new DiffExecutorModule());
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index 2ed62f6..cc503a3 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -10,8 +10,8 @@
 
 maven_jar(
   name = 'collections',
-  id = 'commons-collections:commons-collections:3.2.1',
-  sha1 = '761ea405b9b37ced573d2df0d1e3a4e0f9edc668',
+  id = 'commons-collections:commons-collections:3.2.2',
+  sha1 = '8ad72fe39fa8c91eaaf12aadb21e0c3661fe26d5',
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
   attach_source = False,
diff --git a/lib/js/BUCK b/lib/js/BUCK
index 56d09b6..948f9b4 100644
--- a/lib/js/BUCK
+++ b/lib/js/BUCK
@@ -94,28 +94,28 @@
 bower_component(
   name = 'iron-a11y-keys-behavior',
   package = 'polymerelements/iron-a11y-keys-behavior',
-  version = '1.1.0',
+  version = '1.1.1',
   deps = [':polymer'],
   license = 'polymer',
-  sha1 = '0b7962ed8409336652da4b4e83d052dbe53d4e1a',
+  sha1 = '6bb52b967a4fb242897520dad6c366135e3813ce',
 )
 
 bower_component(
   name = 'iron-ajax',
   package = 'polymerelements/iron-ajax',
-  version = '1.1.0',
+  version = '1.2.0',
   deps = [
     ':polymer',
     ':promise-polyfill',
   ],
   license = 'polymer',
-  sha1 = 'f94a3a3d847842c49def41e27da42c7c94f8d7c7',
+  sha1 = 'f195d0d0ddef73a20573b0a02ce6a505cc1d7014',
 )
 
 bower_component(
   name = 'iron-autogrow-textarea',
   package = 'polymerelements/iron-autogrow-textarea',
-  version = '1.0.10',
+  version = '1.0.12',
   deps = [
     ':iron-behaviors',
     ':iron-flex-layout',
@@ -124,25 +124,25 @@
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = 'd368240e60a4b02ffc731ad8f45f3c8bbf47e9bd',
+  sha1 = 'b9b6874c9a2b5be435557a827ff8bd6661672ee3',
 )
 
 bower_component(
   name = 'iron-behaviors',
   package = 'polymerelements/iron-behaviors',
-  version = '1.0.11',
+  version = '1.0.13',
   deps = [
     ':iron-a11y-keys-behavior',
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = 'e0fcfcd8696381fc78ff62261ba333e5e133f39d',
+  sha1 = 'e9bcdac5414cb8282b5f75eeb51c9154380045af',
 )
 
 bower_component(
   name = 'iron-dropdown',
   package = 'polymerelements/iron-dropdown',
-  version = '1.0.6',
+  version = '1.2.0',
   deps = [
     ':iron-a11y-keys-behavior',
     ':iron-behaviors',
@@ -152,25 +152,25 @@
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = 'b54ff404ce5535919979bb4488e4b6ae9146fc5a',
+  sha1 = 'ca97cbfe5873324ba8af80dbdf79af9e72b6f0b8',
 )
 
 bower_component(
   name = 'iron-fit-behavior',
   package = 'polymerelements/iron-fit-behavior',
-  version = '1.0.5',
+  version = '1.0.6',
   deps = [':polymer'],
   license = 'polymer',
-  sha1 = 'c0273d22531451a1e64f447971ad16b357a7f7e0',
+  sha1 = '28df0349d3cb20ac5e4aeb40651ef7d84de75fb0',
 )
 
 bower_component(
   name = 'iron-flex-layout',
   package = 'polymerelements/iron-flex-layout',
-  version = '1.2.2',
+  version = '1.3.1',
   deps = [':polymer'],
   license = 'polymer',
-  sha1 = '3ca2fbbf3b56d95677663f78304262dee68753c3',
+  sha1 = 'ba696394abff5e799fc06eb11bff4720129a1b52',
 )
 
 bower_component(
@@ -185,13 +185,13 @@
 bower_component(
   name = 'iron-input',
   package = 'polymerelements/iron-input',
-  version = '1.0.6',
+  version = '1.0.8',
   deps = [
     ':iron-validatable-behavior',
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = '2d3eedf0a26046c0e828b1ce3d5b102ee1d0ab19',
+  sha1 = '568c407ffbb524fe2c9ad8230eb895d76c9a8671',
 )
 
 bower_component(
@@ -206,32 +206,33 @@
 bower_component(
   name = 'iron-overlay-behavior',
   package = 'polymerelements/iron-overlay-behavior',
-  version = '1.1.1',
+  version = '1.4.2',
   deps = [
+    ':iron-a11y-keys-behavior',
     ':iron-fit-behavior',
     ':iron-resizable-behavior',
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = '98d80ea1cbee2631553d4fbc98da6cbb25748a4f',
+  sha1 = 'babdd95d7efd63bf3f2969a8f1036e8f324979a9',
 )
 
 bower_component(
   name = 'iron-resizable-behavior',
   package = 'polymerelements/iron-resizable-behavior',
-  version = '1.0.2',
+  version = '1.0.3',
   deps = [':polymer'],
   license = 'polymer',
-  sha1 = '954e82c70b5412d20e7b4d65195a844bb6dc9a07',
+  sha1 = '5982a3e19af7ed3e3de276a9b7bd266b3a144002',
 )
 
 bower_component(
   name = 'iron-selector',
   package = 'polymerelements/iron-selector',
-  version = '1.0.8',
+  version = '1.2.5',
   deps = [':polymer'],
   license = 'polymer',
-  sha1 = '7559560733882656bf479b620669a1d60c3bda21',
+  sha1 = '7728750bc9dfa858915dfd25397709bdbdaee2b1',
 )
 
 bower_component(
@@ -258,7 +259,7 @@
 bower_component(
   name = 'neon-animation',
   package = 'polymerelements/neon-animation',
-  version = '1.0.8',
+  version = '1.1.1',
   deps = [
     ':iron-meta',
     ':iron-resizable-behavior',
@@ -268,7 +269,7 @@
     ':web-animations-js',
   ],
   license = 'polymer',
-  sha1 = 'c5f3700e9259554db14f9dfddb290a42c099d88a',
+  sha1 = 'd6e1b45e5a936d0ec0b66b3520e230e9d8605642',
 )
 
 bower_component(
@@ -282,23 +283,23 @@
 bower_component(
   name = 'paper-styles',
   package = 'polymerelements/paper-styles',
-  version = '1.0.13',
+  version = '1.1.4',
   deps = [
     ':font-roboto',
     ':iron-flex-layout',
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = 'e0bfdadfe10e070f39c16aa784de16734eed25a6',
+  sha1 = '89276c5ec18b8927a704dda2bf14ff35c310401a',
 )
 
 bower_component(
   name = 'polymer',
   package = 'polymer/polymer',
-  version = '1.2.4',
+  version = '1.3.1',
   deps = [':webcomponentsjs'],
   license = 'polymer',
-  sha1 = 'bcc1356d8e7da1a2e339b953e4e056cc13b58bd4',
+  sha1 = '5f54c14f7b8cecdb356e446a84dabb4ba349d278',
 )
 
 bower_component(
@@ -321,15 +322,15 @@
 bower_component(
   name = 'web-animations-js',
   package = 'web-animations/web-animations-js',
-  version = '2.1.2',
+  version = '2.1.4',
   license = 'Apache2.0',
-  sha1 = '3e2f4648b770183f577cb5171785cfedcb3a960b',
+  sha1 = '92f06d8417a51f1f75c94b7a19616e19695cc6db',
 )
 
 bower_component(
   name = 'webcomponentsjs',
   package = 'webcomponentsjs',
-  version = '0.7.20',
+  version = '0.7.21',
   license = 'polymer',
-  sha1 = '95bb33be5aee6af2f49b5d8ca5cfcb4989e89ba8',
+  sha1 = 'ceb96b01c8a86b17831a25d6ab9eca95226c408e',
 )
diff --git a/plugins/replication b/plugins/replication
index 2044446..f74f8f5 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 204444637abfb38254667f73b6cd1242daf19e24
+Subproject commit f74f8f500ee8ea18dca64bada77c8e39e3d1f742
diff --git a/polygerrit-ui/app/BUCK b/polygerrit-ui/app/BUCK
index 93cf614..391aa97 100644
--- a/polygerrit-ui/app/BUCK
+++ b/polygerrit-ui/app/BUCK
@@ -1,6 +1,9 @@
 include_defs('//lib/js.defs')
 
-WCT_TEST_PATTERNS = ['test/**']
+WCT_TEST_PATTERNS = [
+  'test/**',
+  '**/*_test.html',
+]
 PY_TEST_PATTERNS = ['polygerrit_wct_tests.py']
 APP_SRCS = glob(
   ['**'],
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior.html
index 55cd2b1..f6897cd 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior.html
@@ -98,6 +98,10 @@
       }
       return v;
     },
+
+    changePath: function(changeNum) {
+      return '/c/' + changeNum;
+    },
   };
 
   window.Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 0a4aec4..39dda7f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -20,7 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-change-list-item.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index acb8789..ca9da5b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -21,7 +21,7 @@
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
 <script src="../../../bower_components/page/page.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
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 1593fab..582a28a 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
@@ -23,6 +23,7 @@
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-request/gr-request.html">
 
+<link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html">
 <link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
 
 <dom-module id="gr-change-actions">
@@ -78,6 +79,12 @@
           on-confirm="_handleRebaseConfirm"
           on-cancel="_handleConfirmDialogCancel"
           hidden></gr-confirm-rebase-dialog>
+      <gr-confirm-cherrypick-dialog id="confirmCherrypick"
+          class="confirmDialog"
+          message="[[commitMessage]]"
+          on-confirm="_handleCherrypickConfirm"
+          on-cancel="_handleConfirmDialogCancel"
+          hidden></gr-confirm-cherrypick-dialog>
     </gr-overlay>
   </template>
   <script src="gr-change-actions.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index a89c0aa..04b9bb5 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
@@ -23,6 +23,7 @@
 
   // TODO(andybons): Add the rest of the revision actions.
   var RevisionActions = {
+    CHERRYPICK: 'cherrypick',
     DELETE: '/',
     PUBLISH: 'publish',
     REBASE: 'rebase',
@@ -44,6 +45,7 @@
       },
       changeNum: String,
       patchNum: String,
+      commitMessage: String,
       _loading: {
         type: Boolean,
         value: true,
@@ -67,12 +69,7 @@
     },
 
     _actionsChanged: function(actions, revisionActions) {
-      this.hidden =
-          revisionActions.rebase == null &&
-          revisionActions.submit == null &&
-          revisionActions.publish == null &&
-          actions.abandon == null &&
-          actions.restore == null;
+      this.hidden = actions.length == 0 && revisionActions.length == 0;
     },
 
     _computeRevisionActionsPath: function(changeNum, patchNum) {
@@ -100,6 +97,7 @@
 
     _computeLoadingLabel: function(action) {
       return {
+        'cherrypick': 'Cherry-Picking...',
         'rebase': 'Rebasing...',
         'submit': 'Submitting...',
       }[action];
@@ -124,7 +122,10 @@
       var type = el.getAttribute('data-action-type');
       if (type == 'revision') {
         if (key == RevisionActions.REBASE) {
-          this._showRebaseDialog();
+          this._showActionDialog(this.$.confirmRebase);
+          return;
+        } else if (key == RevisionActions.CHERRYPICK) {
+          this._showActionDialog(this.$.confirmCherrypick);
           return;
         }
         this._fireRevisionAction(this._prependSlash(key),
@@ -167,6 +168,28 @@
           payload);
     },
 
+    _handleCherrypickConfirm: function() {
+      var el = this.$.confirmCherrypick;
+      if (!el.branch) {
+        // TODO(davido): Fix error handling
+        alert('The destination branch can’t be empty.');
+        return;
+      }
+      if (!el.message) {
+        alert('The commit message can’t be empty.');
+        return;
+      }
+      this.$.overlay.close();
+      el.hidden = false;
+      this._fireRevisionAction('/cherrypick',
+          this._revisionActions.cherrypick,
+          {
+            destination: el.branch,
+            message: el.message,
+          }
+      );
+    },
+
     _fireChangeAction: function(endpoint, action) {
       this._send(action.method, {}, endpoint).then(
         function() {
@@ -193,8 +216,12 @@
       }
 
       this._send(action.method, opt_payload, endpoint, true).then(
-        function() {
-          this.fire('reload-change', null, {bubbles: false});
+        function(req) {
+          if (action.__key == RevisionActions.CHERRYPICK) {
+            page.show(this.changePath(req.response._number));
+          } else {
+            this.fire('reload-change', null, {bubbles: false});
+          }
           enableButton();
         }.bind(this)).catch(function(err) {
           // TODO(andybons): Handle merge conflict (409 status);
@@ -205,8 +232,8 @@
         });
     },
 
-    _showRebaseDialog: function() {
-      this.$.confirmRebase.hidden = false;
+    _showActionDialog: function(dialog) {
+      dialog.hidden = false;
       this.$.overlay.open();
     },
 
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 1c5eb9c..cc94404 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
@@ -36,7 +36,7 @@
     var element;
     var server;
 
-    setup(function() {
+    setup(function(done) {
       element = fixture('basic');
       server = sinon.fakeServer.create();
 
@@ -91,15 +91,17 @@
 
       element.changeNum = '42';
       element.patchNum = '2';
-      element.reload();
+      element.reload().then(function() {
+        done();
+      });
 
       server.respond();
     });
 
-    test('submit and rebase buttons show', function(done) {
+    test('submit, rebase, and cherry-pick buttons show', function(done) {
       flush(function() {
         var buttonEls = Polymer.dom(element.root).querySelectorAll('gr-button');
-        assert.equal(buttonEls.length, 2);
+        assert.equal(buttonEls.length, 3);
         assert.isFalse(element.hidden);
         done();
       });
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 7b1a2f1..22cf176 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -20,8 +20,6 @@
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
 
-<script src="../../../scripts/fake-app.js"></script>
-
 <dom-module id="gr-change-metadata">
   <template>
     <style>
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 6c97b5a..f437dbc 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
@@ -24,6 +24,7 @@
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-change-metadata.html">
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <test-fixture id="basic">
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 1dbdbfb..a394b2b 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
@@ -21,9 +21,10 @@
 <link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-change-star/gr-change-star.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <link rel="import" href="../gr-change-actions/gr-change-actions.html">
 <link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
@@ -79,7 +80,7 @@
       }
       .download,
       .patchSelectLabel {
-        margin-left: var(--default-horizontal-margin);
+        margin-left: 1em;
       }
       .header select {
         margin-left: .5em;
@@ -88,8 +89,7 @@
         margin-left: var(--default-horizontal-margin);
       }
       gr-reply-dialog {
-        min-width: 30em;
-        max-width: 50em;
+        width: 50em;
       }
       .changeStatus {
         color: #999;
@@ -243,8 +243,12 @@
             <span class="changeStatus">[[_computeChangeStatus(_change, _patchNum)]]</span>
           </span>
           <span class="header-actions">
-            <gr-button class="reply" hidden$="[[!_loggedIn]]" hidden on-tap="_handleReplyTap">Reply</gr-button>
-            <gr-button link class="download" on-tap="_handleDownloadTap">Download</gr-button>
+            <gr-button hidden
+                class="reply"
+                primary$="[[_computeReplyButtonHighlighted(_diffDrafts)]]"
+                hidden$="[[!_loggedIn]]"
+                on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
+            <gr-button class="download" on-tap="_handleDownloadTap">Download</gr-button>
             <span>
               <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
               <select id="patchSetSelect" on-change="_handlePatchChange">
@@ -269,6 +273,7 @@
               actions="[[_change.actions]]"
               change-num="[[_changeNum]]"
               patch-num="[[_patchNum]]"
+              commit-message="[[_commitInfo.message]]"
               on-reload-change="_handleReloadChange"></gr-change-actions>
         </div>
         <div class="changeInfo-column commitAndRelated">
@@ -290,6 +295,7 @@
           change-num="[[_changeNum]]"
           patch-num="[[_patchNum]]"
           comments="[[_comments]]"
+          drafts="[[_diffDrafts]]"
           selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
       <gr-messages-list id="messageList"
           change-num="[[_changeNum]]"
@@ -314,10 +320,12 @@
           patch-num="[[_patchNum]]"
           labels="[[_change.labels]]"
           permitted-labels="[[_change.permitted_labels]]"
+          diff-drafts="[[_diffDrafts]]"
           on-send="_handleReplySent"
           on-cancel="_handleReplyCancel"
           hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog>
     </gr-overlay>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-change-view.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index a42a379..9af6aa9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -49,6 +49,7 @@
       },
       _commitInfo: Object,
       _changeNum: String,
+      _diffDrafts: Object,
       _patchNum: String,
       _allPatchSets: {
         type: Array,
@@ -66,6 +67,11 @@
         type: Function,
         value: function() { return this._handleBodyScroll.bind(this); },
       },
+      _replyButtonLabel: {
+        type: String,
+        value: 'Reply',
+        computed: '_computeReplyButtonLabel(_diffDrafts)',
+      },
     },
 
     behaviors: [
@@ -74,13 +80,14 @@
     ],
 
     ready: function() {
-      app.accountReady.then(function() {
-        this._loggedIn = app.loggedIn;
-      }.bind(this));
       this._headerEl = this.$$('.header');
     },
 
     attached: function() {
+      this._getLoggedIn().then(function(loggedIn) {
+        this._loggedIn = loggedIn;
+      }.bind(this));
+
       window.addEventListener('scroll', this._boundScrollHandler);
     },
 
@@ -116,10 +123,10 @@
       var currentPatchNum =
           this._change.revisions[this._change.current_revision]._number;
       if (patchNum == currentPatchNum) {
-        page.show(this._computeChangePath(this._changeNum));
+        page.show(this.changePath(this._changeNum));
         return;
       }
-      page.show(this._computeChangePath(this._changeNum) + '/' + patchNum);
+      page.show(this.changePath(this._changeNum) + '/' + patchNum);
     },
 
     _handleReplyTap: function(e) {
@@ -145,9 +152,6 @@
     },
 
     _handleReplyOverlayOpen: function(e) {
-      this.$.replyDialog.reload().then(function() {
-        this.async(function() { this.$.replyOverlay.center() }, 1);
-      }.bind(this));
       this.$.replyDialog.focus();
     },
 
@@ -186,8 +190,8 @@
           }
         }.bind(this), 1);
 
-        app.accountReady.then(function() {
-          if (!this._loggedIn) { return; }
+        this._getLoggedIn().then(function(loggedIn) {
+          if (!loggedIn) { return; }
 
           if (this.viewState.showReplyDialog) {
             this.$.replyOverlay.open();
@@ -206,10 +210,6 @@
       this.fire('title-change', {title: title});
     },
 
-    _computeChangePath: function(changeNum) {
-      return '/c/' + changeNum;
-    },
-
     _computeChangePermalink: function(changeNum) {
       return '/' + changeNum;
     },
@@ -305,11 +305,30 @@
       return result;
     },
 
+    _computeReplyButtonHighlighted: function(drafts) {
+      return Object.keys(drafts || {}).length > 0;
+    },
+
+    _computeReplyButtonLabel: function(drafts) {
+      drafts = drafts || {};
+      var draftCount = Object.keys(drafts).reduce(function(count, file) {
+        return count + drafts[file].length;
+      }, 0);
+
+      var label = 'Reply';
+      if (draftCount > 0) {
+        label += ' (' + draftCount + ')';
+      }
+      return label;
+    },
+
     _handleKey: function(e) {
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
 
       switch (e.keyCode) {
         case 65:  // 'a'
+          if (!this._loggedIn) { return; }
+
           e.preventDefault();
           this.$.replyOverlay.open();
           break;
@@ -321,12 +340,37 @@
     },
 
     _handleReloadChange: function() {
-      page.show(this._computeChangePath(this._changeNum));
+      page.show(this.changePath(this._changeNum));
+    },
+
+    _getDiffDrafts: function() {
+      return this.$.restAPI.getDiffDrafts(this._changeNum).then(
+          function(drafts) { return this._diffDrafts = drafts; }.bind(this));
+    },
+
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
+    },
+
+    _reloadDiffDrafts: function() {
+      this._diffDrafts = {};
+      this._getDiffDrafts().then(function() {
+        if (this.$.replyOverlay.opened) {
+          this.async(function() { this.$.replyOverlay.center(); }, 1);
+        }
+      }.bind(this));
     },
 
     _reload: function() {
+      this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) { return; }
+
+        this._reloadDiffDrafts();
+      }.bind(this));
+
       var detailCompletes = this.$.detailXHR.generateRequest().completes;
       this.$.commentsXHR.generateRequest();
+
       var reloadPatchNumDependentResources = function() {
         return Promise.all([
           this.$.commitInfoXHR.generateRequest().completes,
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index ed9d28d..af14e06 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -21,7 +21,7 @@
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
 <script src="../../../bower_components/page/page.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
@@ -67,13 +67,37 @@
           'Should navigate to /');
       showStub.restore();
 
+
       MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
       var overlayEl = element.$.replyOverlay;
+      assert.isFalse(overlayEl.opened);
+      element._loggedIn = true;
+
+      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'a'
       assert.isTrue(overlayEl.opened);
       overlayEl.close();
       assert.isFalse(overlayEl.opened);
     });
 
+    test('reply button is highlighted when there are drafts', function() {
+      var replyButton = element.$$('gr-button.reply');
+      assert.ok(replyButton);
+      assert.isFalse(replyButton.hasAttribute('primary'));
+
+      element._diffDrafts = null;
+      assert.isFalse(replyButton.hasAttribute('primary'));
+
+      element._diffDrafts = {};
+      assert.isFalse(replyButton.hasAttribute('primary'));
+
+      element._diffDrafts = {
+        'file1.txt': [{}],
+        'file2.txt': [{}, {}],
+      };
+      assert.isTrue(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply (3)');
+    });
+
     test('patch num change', function(done) {
       element._changeNum = '42';
       element._patchNum = 2;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
new file mode 100644
index 0000000..b21575b
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
@@ -0,0 +1,74 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
+
+<dom-module id="gr-confirm-cherrypick-dialog">
+  <template>
+    <style>
+      :host {
+        display: block;
+        width: 30em;
+      }
+      :host([disabled]) {
+        opacity: .5;
+        pointer-events: none;
+      }
+      label {
+        cursor: pointer;
+      }
+      iron-autogrow-textarea {
+        padding: 0;
+      }
+      .main label,
+      .main input[type="text"] {
+        display: block;
+        font: inherit;
+        width: 100%;
+      }
+      .main .message {
+        border: groove;
+        width: 100%;
+      }
+    </style>
+    <gr-confirm-dialog
+        confirm-label="Cherry Pick"
+        on-confirm="_handleConfirmTap"
+        on-cancel="_handleCancelTap">
+      <div class="header">Cherry Pick Change to Another Branch</div>
+      <div class="main">
+        <label for="branchInput">
+          Cherry Pick to branch
+        </label>
+        <input is="iron-input"
+            type="text"
+            id="branchInput"
+            bind-value="{{branch}}"
+            placeholder="Destination branch">
+        <label for="messageInput">
+          Cherry Pick Commit Message
+        </label>
+        <iron-autogrow-textarea
+            id="messageInput"
+            class="message"
+            bind-value="{{message}}"></iron-autogrow-textarea>
+      </div>
+    </gr-confirm-dialog>
+  </template>
+  <script src="gr-confirm-cherrypick-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
new file mode 100644
index 0000000..f27e4e2
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -0,0 +1,47 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-confirm-cherrypick-dialog',
+
+    /**
+     * Fired when the confirm button is pressed.
+     *
+     * @event confirm
+     */
+
+    /**
+     * Fired when the cancel button is pressed.
+     *
+     * @event cancel
+     */
+
+    properties: {
+      branch: String,
+      message: String,
+    },
+
+    _handleConfirmTap: function(e) {
+      e.preventDefault();
+      this.fire('confirm', null, {bubbles: false});
+    },
+
+    _handleCancelTap: function(e) {
+      e.preventDefault();
+      this.fire('cancel', null, {bubbles: false});
+    },
+  });
+})();
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 e010468..d94a828 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
@@ -16,9 +16,8 @@
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
-<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="../../shared/gr-request/gr-request.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-file-list">
   <template>
@@ -49,6 +48,9 @@
         visibility: hidden;
         width: 1.25em;
       }
+      .row:not(.header):hover {
+        background-color: #f5fafd;
+      }
       .row[selected] {
         background-color: #ebf5fb;
       }
@@ -112,17 +114,6 @@
         }
       }
     </style>
-    <gr-ajax id="filesXHR"
-        url="[[_computeFilesURL(changeNum, patchNum)]]"
-        on-response="_handleResponse"></gr-ajax>
-    <gr-ajax id="draftsXHR"
-        url="[[_computeDraftsURL(changeNum, patchNum)]]"
-        last-response="{{_drafts}}"></gr-ajax>
-    <gr-ajax id="reviewedXHR"
-        url="[[_computeReviewedURL(changeNum, patchNum)]]"
-        last-response="{{_reviewed}}"></gr-ajax>
-    </gr-ajax>
-
     <div class="row header">
       <div class="positionIndicator"></div>
       <div class="reviewed" hidden$="[[!_loggedIn]]" hidden></div>
@@ -131,7 +122,7 @@
       <div class="comments">Comments</div>
       <div class="stats">Stats</div>
     </div>
-    <template is="dom-repeat" items="{{files}}" as="file">
+    <template is="dom-repeat" items="{{_files}}" as="file">
       <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
         <div class="positionIndicator">&#x25b6;</div>
         <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
@@ -145,7 +136,7 @@
           [[_computeFileDisplayName(file.__path)]]
         </a>
         <div class="comments">
-          <span class="drafts">[[_computeDraftsString(_drafts, file.__path)]]</span>
+          <span class="drafts">[[_computeDraftsString(drafts, patchNum, file.__path)]]</span>
           [[_computeCommentsString(comments, patchNum, file.__path)]]
         </div>
         <div class$="[[_computeClass('stats', file.__path)]]">
@@ -154,6 +145,7 @@
         </div>
       </div>
     </template>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-file-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 3c66289..39e50a9 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -23,7 +23,7 @@
       patchNum: String,
       changeNum: String,
       comments: Object,
-      files: Array,
+      drafts: Object,
       selectedIndex: {
         type: Number,
         notify: true,
@@ -33,107 +33,114 @@
         value: function() { return document.body; },
       },
 
+      _files: Array,
       _loggedIn: {
         type: Boolean,
         value: false,
       },
-      _drafts: Object,
       _reviewed: {
         type: Array,
         value: function() { return []; },
       },
-      _filesRequestPromise: Object,  // Used for testing.
-      _reviewedRequestPromise: Object,  // Used for testing.
-      _xhrPromise: Object,  // Used for testing.
     },
 
     behaviors: [
       Gerrit.KeyboardShortcutBehavior,
-      Gerrit.RESTClientBehavior,
     ],
 
     reload: function() {
       if (!this.changeNum || !this.patchNum) {
         return Promise.resolve();
       }
-      return Promise.all([
-        this._filesRequestPromise =
-            this.$.filesXHR.generateRequest().completes,
-        app.accountReady.then(function() {
-          this._loggedIn = app.loggedIn;
-          if (!app.loggedIn) { return; }
-          this.$.draftsXHR.generateRequest();
-          this._reviewedRequestPromise =
-              this.$.reviewedXHR.generateRequest().completes;
-        }.bind(this)),
-      ]);
-    },
 
-    _computeFilesURL: function(changeNum, patchNum) {
-      return this.changeBaseURL(changeNum, patchNum) + '/files';
+      var promises = [];
+      var _this = this;
+
+      promises.push(this._getFiles().then(function(files) {
+        _this._files = files;
+      }));
+      promises.push(this._getLoggedIn().then(function(loggedIn) {
+        return _this._loggedIn = loggedIn;
+      }).then(function(loggedIn) {
+        if (!loggedIn) { return; }
+
+        return _this._getReviewedFiles().then(function(reviewed) {
+          _this._reviewed = reviewed;
+        });
+      }));
+
+      return Promise.all(promises);
     },
 
     _computeCommentsString: function(comments, patchNum, path) {
-      var patchComments = (comments[path] || []).filter(function(c) {
-        return c.patch_set == patchNum;
-      });
-      var num = patchComments.length;
-      if (num == 0) { return ''; }
-      if (num == 1) { return '1 comment'; }
-      if (num > 1) { return num + ' comments'; }
+      return this._computeCountString(comments, patchNum, path, 'comment');
     },
 
-    _computeReviewedURL: function(changeNum, patchNum) {
-      return this.changeBaseURL(changeNum, patchNum) + '/files?reviewed';
+    _computeDraftsString: function(drafts, patchNum, path) {
+      return this._computeCountString(drafts, patchNum, path, 'draft');
+    },
+
+    _computeCountString: function(comments, patchNum, path, noun) {
+      if (!comments) { return ''; }
+
+      var patchComments = (comments[path] || []).filter(function(c) {
+        return parseInt(c.patch_set, 10) === parseInt(patchNum, 10);
+      });
+      var num = patchComments.length;
+      if (num === 0) { return ''; }
+      return num + ' ' + noun + (num > 1 ? 's' : '');
     },
 
     _computeReviewed: function(file, _reviewed) {
-      return _reviewed.indexOf(file.__path) != -1;
+      return _reviewed.indexOf(file.__path) !== -1;
     },
 
     _handleReviewedChange: function(e) {
       var path = Polymer.dom(e).rootTarget.getAttribute('data-path');
       var index = this._reviewed.indexOf(path);
-      var reviewed = index != -1;
+      var reviewed = index !== -1;
       if (reviewed) {
         this.splice('_reviewed', index, 1);
       } else {
         this.push('_reviewed', path);
       }
 
-      var method = reviewed ? 'DELETE' : 'PUT';
-      var url = this.changeBaseURL(this.changeNum, this.patchNum) +
-          '/files/' + encodeURIComponent(path) + '/reviewed';
-      this._send(method, url).catch(function(err) {
+      this._saveReviewedState(path, !reviewed).catch(function(err) {
         alert('Couldn’t change file review status. Check the console ' +
             'and contact the PolyGerrit team for assistance.');
         throw err;
       }.bind(this));
     },
 
-    _computeDraftsURL: function(changeNum, patchNum) {
-      return this.changeBaseURL(changeNum, patchNum) + '/drafts';
+    _saveReviewedState: function(path, reviewed) {
+      return this.$.restAPI.saveFileReviewed(this.changeNum, this.patchNum,
+          path, reviewed);
     },
 
-    _computeDraftsString: function(drafts, path) {
-      var num = (drafts[path] || []).length;
-      if (num == 0) { return ''; }
-      if (num == 1) { return '1 draft'; }
-      if (num > 1) { return num + ' drafts'; }
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
     },
 
-    _handleResponse: function(e, req) {
-      var result = e.detail.response;
-      var paths = Object.keys(result).sort();
+    _getReviewedFiles: function() {
+      return this.$.restAPI.getReviewedFiles(this.changeNum, this.patchNum);
+    },
+
+    _getFiles: function() {
+      return this.$.restAPI.getChangeFiles(this.changeNum, this.patchNum).then(
+          this._normalizeFilesResponse.bind(this));
+    },
+
+    _normalizeFilesResponse: function(response) {
+      var paths = Object.keys(response).sort();
       var files = [];
       for (var i = 0; i < paths.length; i++) {
-        var info = result[paths[i]];
+        var info = response[paths[i]];
         info.__path = paths[i];
         info.lines_inserted = info.lines_inserted || 0;
         info.lines_deleted = info.lines_deleted || 0;
         files.push(info);
       }
-      this.files = files;
+      return files;
     },
 
     _handleKey: function(e) {
@@ -143,7 +150,7 @@
         case 74:  // 'j'
           e.preventDefault();
           this.selectedIndex =
-              Math.min(this.files.length - 1, this.selectedIndex + 1);
+              Math.min(this._files.length - 1, this.selectedIndex + 1);
           break;
         case 75:  // 'k'
           e.preventDefault();
@@ -151,7 +158,7 @@
           break;
         case 219:  // '['
           e.preventDefault();
-          this._openSelectedFile(this.files.length - 1);
+          this._openSelectedFile(this._files.length - 1);
           break;
         case 221:  // ']'
           e.preventDefault();
@@ -170,11 +177,11 @@
         this.selectedIndex = opt_index;
       }
       page.show(this._computeDiffURL(this.changeNum, this.patchNum,
-          this.files[this.selectedIndex].__path));
+          this._files[this.selectedIndex].__path));
     },
 
     _computeFileSelected: function(index, selectedIndex) {
-      return index == selectedIndex;
+      return index === selectedIndex;
     },
 
     _computeFileStatus: function(status) {
@@ -186,24 +193,15 @@
     },
 
     _computeFileDisplayName: function(path) {
-      return path == COMMIT_MESSAGE_PATH ? 'Commit message' : path;
+      return path === COMMIT_MESSAGE_PATH ? 'Commit message' : path;
     },
 
     _computeClass: function(baseClass, path) {
       var classes = [baseClass];
-      if (path == COMMIT_MESSAGE_PATH) {
+      if (path === COMMIT_MESSAGE_PATH) {
         classes.push('invisible');
       }
       return classes.join(' ');
     },
-
-    _send: function(method, url) {
-      var xhr = document.createElement('gr-request');
-      this._xhrPromise = xhr.send({
-        method: method,
-        url: url,
-      });
-      return this._xhrPromise;
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index a1140f5..21263c0 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -21,7 +21,7 @@
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
 <script src="../../../bower_components/page/page.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
@@ -36,204 +36,93 @@
 <script>
   suite('gr-file-list tests', function() {
     var element;
-    var server;
+    var getLoggedInStub;
 
     setup(function() {
       element = fixture('basic');
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/1/files',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify({
-            '/COMMIT_MSG': {
-              status: 'A',
-              lines_inserted: 9,
-              size_delta: 317,
-              size: 317
-            },
-            'myfile.txt': {
-              lines_inserted: 35,
-              size_delta: 1146,
-              size: 1167
-            }
-          }),
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/2/files',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify({
-            '/COMMIT_MSG': {
-              status: 'A',
-              lines_inserted: 9,
-              size_delta: 317,
-              size: 317
-            },
-            'myfile.txt': {
-              lines_inserted: 35,
-              size_delta: 1146,
-              size: 1167
-            },
-            'file_added_in_rev2.txt': {
-              lines_inserted: 98,
-              size_delta: 234,
-              size: 136
-            }
-          }),
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/1/drafts',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '{}',
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/2/drafts',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '{}',
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/1/files?reviewed',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '["/COMMIT_MSG"]',
-        ]
-      );
-      server.respondWith(
-        'GET',
-        '/changes/42/revisions/2/files?reviewed',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '["/COMMIT_MSG","myfile.txt"]',
-        ]
-      );
-      server.respondWith(
-        'PUT',
-        '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed',
-        [
-          201,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          '""',
-        ]
-      );
-      server.respondWith(
-        'DELETE',
-        '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed',
-        [
-          204,
-          {'Content-Type': 'application/json'},
-          '',
-        ]
-      );
-
-      app.loggedIn = true;
+      getLoggedInStub = sinon.stub(element, '_getLoggedIn', function() {
+        return Promise.resolve(true);
+      });
     });
 
     teardown(function() {
-      server.restore();
+      getLoggedInStub.restore();
     });
 
-    test('requests', function(done) {
-      element.changeNum = '42';
-      element.patchNum = '1';
-      element.reload();
-      server.respond();
-
-      Promise.all([
-          element._filesRequestPromise,
-          element._reviewedRequestPromise,
-      ]).then(function() {
-        flushAsynchronousOperations();
-        var filenames = element.files.map(function(f) {
-          return f.__path;
-        });
-        assert.deepEqual(filenames, ['/COMMIT_MSG', 'myfile.txt']);
-        assert.deepEqual(element._reviewed, ['/COMMIT_MSG']);
-
-        element.patchNum = '2';
-        element.reload();
-        server.respond();
-        Promise.all([
-            element._filesRequestPromise,
-            element._reviewedRequestPromise,
-        ]).then(function() {
-          flushAsynchronousOperations();
-          filenames = element.files.map(function(f) {
-            return f.__path;
+    test('get file list', function(done) {
+      var getChangeFilesStub = sinon.stub(element.$.restAPI, 'getChangeFiles',
+          function() {
+            return Promise.resolve({
+              '/COMMIT_MSG': {lines_inserted: 9},
+              'tags.html': {lines_deleted: 123},
+              'about.txt': {},
+            });
           });
-          assert.deepEqual(filenames,
-              ['/COMMIT_MSG', 'file_added_in_rev2.txt', 'myfile.txt']);
-          assert.deepEqual(element._reviewed, ['/COMMIT_MSG', 'myfile.txt']);
-          done();
+
+      element._getFiles().then(function(files) {
+        var filenames = files.map(function(f) { return f.__path; });
+        assert.deepEqual(filenames, ['/COMMIT_MSG', 'about.txt', 'tags.html']);
+        assert.deepEqual(files[0], {
+          lines_inserted: 9,
+          lines_deleted: 0,
+          __path: '/COMMIT_MSG',
         });
-      });
-    });
+        assert.deepEqual(files[1], {
+          lines_inserted: 0,
+          lines_deleted: 0,
+          __path: 'about.txt',
+        });
+        assert.deepEqual(files[2], {
+          lines_inserted: 0,
+          lines_deleted: 123,
+          __path: 'tags.html',
+        });
 
-    test('keyboard shortcuts', function(done) {
-      element.changeNum = '42';
-      element.patchNum = '2';
-      element.selectedIndex = 0;
-      element.reload();
-      server.respond();
-
-      element._filesRequestPromise.then(function() {
-        flushAsynchronousOperations();
-        var elementItems = Polymer.dom(element.root).querySelectorAll(
-            '.row:not(.header)');
-        assert.equal(elementItems.length, 3);
-        assert.isTrue(elementItems[0].hasAttribute('selected'));
-        assert.isFalse(elementItems[1].hasAttribute('selected'));
-        assert.isFalse(elementItems[2].hasAttribute('selected'));
-        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
-        assert.equal(element.selectedIndex, 1);
-        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
-
-        var showStub = sinon.stub(page, 'show');
-        assert.equal(element.selectedIndex, 2);
-        MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'enter'
-        assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
-            'Should navigate to /c/42/2/myfile.txt');
-
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        assert.equal(element.selectedIndex, 1);
-        MockInteractions.pressAndReleaseKeyOn(element, 79);  // 'o'
-        assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
-            'Should navigate to /c/42/2/file_added_in_rev2.txt');
-
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
-        assert.equal(element.selectedIndex, 0);
-
-        showStub.restore();
         done();
       });
     });
 
+    test('keyboard shortcuts', function() {
+      element._files = [
+        {__path: '/COMMIT_MSG'},
+        {__path: 'file_added_in_rev2.txt'},
+        {__path: 'myfile.txt'},
+      ];
+      element.changeNum = '42',
+      element.patchNum = '2';
+      element.selectedIndex = 0;
+
+      flushAsynchronousOperations();
+      var elementItems = Polymer.dom(element.root).querySelectorAll(
+          '.row:not(.header)');
+      assert.equal(elementItems.length, 3);
+      assert.isTrue(elementItems[0].hasAttribute('selected'));
+      assert.isFalse(elementItems[1].hasAttribute('selected'));
+      assert.isFalse(elementItems[2].hasAttribute('selected'));
+      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
+      assert.equal(element.selectedIndex, 1);
+      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'j'
+
+      var showStub = sinon.stub(page, 'show');
+      assert.equal(element.selectedIndex, 2);
+      MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'enter'
+      assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
+          'Should navigate to /c/42/2/myfile.txt');
+
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      assert.equal(element.selectedIndex, 1);
+      MockInteractions.pressAndReleaseKeyOn(element, 79);  // 'o'
+      assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
+          'Should navigate to /c/42/2/file_added_in_rev2.txt');
+
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'k'
+      assert.equal(element.selectedIndex, 0);
+
+      showStub.restore();
+    });
+
     test('comment filtering', function() {
       var comments = {
         '/COMMIT_MSG': [
@@ -248,24 +137,24 @@
         ],
       };
       assert.equal(
-          element._computeCommentsString(comments, '1', '/COMMIT_MSG'),
+          element._computeCountString(comments, '1', '/COMMIT_MSG', 'comment'),
           '2 comments');
       assert.equal(
-          element._computeCommentsString(comments, '1', 'myfile.txt'),
+          element._computeCountString(comments, '1', 'myfile.txt', 'comment'),
           '1 comment');
       assert.equal(
-          element._computeCommentsString(comments, '1',
-              'file_added_in_rev2.txt'),
+          element._computeCountString(comments, '1',
+              'file_added_in_rev2.txt', 'comment'),
           '');
       assert.equal(
-          element._computeCommentsString(comments, '2', '/COMMIT_MSG'),
+          element._computeCountString(comments, '2', '/COMMIT_MSG', 'comment'),
           '1 comment');
       assert.equal(
-          element._computeCommentsString(comments, '2', 'myfile.txt'),
+          element._computeCountString(comments, '2', 'myfile.txt', 'comment'),
           '2 comments');
       assert.equal(
-          element._computeCommentsString(comments, '2',
-              'file_added_in_rev2.txt'),
+          element._computeCountString(comments, '2',
+              'file_added_in_rev2.txt', 'comment'),
           '');
     });
 
@@ -284,50 +173,37 @@
           'clazz invisible');
     });
 
-    test('file review status', function(done) {
+    test('file review status', function() {
+      element._files = [
+        {__path: '/COMMIT_MSG'},
+        {__path: 'file_added_in_rev2.txt'},
+        {__path: 'myfile.txt'},
+      ];
+      element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
       element.changeNum = '42';
       element.patchNum = '2';
-      element.reload();
-      server.respond();
+      element.selectedIndex = 0;
 
-      Promise.all([
-          element._filesRequestPromise,
-          element._reviewedRequestPromise,
-      ]).then(function() {
-        flushAsynchronousOperations();
-        var fileRows =
-            Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
-        var commitMsg = fileRows[0].querySelector('input[type="checkbox"]');
-        var fileAdded = fileRows[1].querySelector('input[type="checkbox"]');
-        var myFile = fileRows[2].querySelector('input[type="checkbox"]');
+      flushAsynchronousOperations();
+      var fileRows =
+          Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+      var commitMsg = fileRows[0].querySelector('input[type="checkbox"]');
+      var fileAdded = fileRows[1].querySelector('input[type="checkbox"]');
+      var myFile = fileRows[2].querySelector('input[type="checkbox"]');
 
-        assert.isTrue(commitMsg.checked);
-        assert.isFalse(fileAdded.checked);
-        assert.isTrue(myFile.checked);
+      assert.isTrue(commitMsg.checked);
+      assert.isFalse(fileAdded.checked);
+      assert.isTrue(myFile.checked);
 
-        assert.equal(element._reviewed.length, 2);
+      var saveStub = sinon.stub(element, '_saveReviewedState',
+          function() { return Promise.resolve(); });
 
-        MockInteractions.tap(commitMsg);
-        server.respond();
-        element._xhrPromise.then(function(req) {
-          assert.equal(element._reviewed.length, 1);
-          assert.equal(req.status, 204);
-          assert.equal(req.url,
-              '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed');
+      MockInteractions.tap(commitMsg);
+      assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false));
+      MockInteractions.tap(commitMsg);
+      assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true));
 
-          MockInteractions.tap(commitMsg);
-          server.respond();
-        }).then(function() {
-          element._xhrPromise.then(function(req) {
-            assert.equal(element._reviewed.length, 2);
-            assert.equal(req.status, 201);
-            assert.equal(req.url,
-                '/changes/42/revisions/2/files/%2FCOMMIT_MSG/reviewed');
-
-            done();
-          });
-        });
-      });
+      saveStub.restore();
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 5733acd..7c287db 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -19,6 +19,7 @@
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
 <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <link rel="import" href="../gr-comment-list/gr-comment-list.html">
 
@@ -120,6 +121,7 @@
         <gr-button small on-tap="_handleReplyTap">Reply</gr-button>
       </div>
     </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-message.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 1ab5e6c..26b9fb9 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -57,7 +57,7 @@
     },
 
     ready: function() {
-      app.configReady.then(function(cfg) {
+      this.$.restAPI.getConfig().then(function(cfg) {
         this.showAvatar = !!(cfg && cfg.plugin && cfg.plugin.has_avatars) &&
             this.message && this.message.author;
       }.bind(this));
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 0f09b70..dc4464b 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -20,7 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index 5a562ba..4f18439 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -20,7 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index ab21e6c..1a2fbab 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -18,7 +18,6 @@
 <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
-<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-request/gr-request.html">
 
@@ -43,13 +42,13 @@
       border-top: 1px solid #ddd;
       padding: .5em .75em;
     }
-    .textareaContainer,
     .labelsContainer,
     .actionsContainer {
       flex-shrink: 0;
     }
     .textareaContainer {
       position: relative;
+      display: flex;
     }
     iron-autogrow-textarea {
       padding: 0;
@@ -101,9 +100,6 @@
     }
   </style>
   <template>
-    <gr-ajax id="draftsXHR"
-        url="[[_computeDraftsURL(changeNum)]]"
-        last-response="{{_drafts}}"></gr-ajax>
     <div class="container">
       <section class="textareaContainer">
         <iron-autogrow-textarea
@@ -131,10 +127,10 @@
           </div>
         </template>
       </section>
-      <section class="draftsContainer" hidden$="[[_computeHideDraftList(_drafts)]]">
-        <h3>[[_computeDraftsTitle(_drafts)]]</h3>
+      <section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]">
+        <h3>[[_computeDraftsTitle(diffDrafts)]]</h3>
         <gr-comment-list
-            comments="[[_drafts]]"
+            comments="[[diffDrafts]]"
             change-num="[[changeNum]]"
             patch-num="[[patchNum]]"></gr-comment-list>
       </section>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 3cd6e12..6ec4d87 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -41,11 +41,11 @@
         type: String,
         value: '',
       },
+      diffDrafts: Object,
       labels: Object,
       permittedLabels: Object,
 
       _account: Object,
-      _drafts: Object,
       _xhrPromise: Object,  // Used for testing.
     },
 
@@ -54,25 +54,17 @@
     ],
 
     ready: function() {
-      app.accountReady.then(function() {
-        this._account = app.account;
+      app.accountReady.then(function(account) {
+        this._account = account;
       }.bind(this));
     },
 
-    reload: function() {
-      return this.$.draftsXHR.generateRequest().completes;
-    },
-
     focus: function() {
       this.async(function() {
         this.$.textarea.textarea.focus();
       }.bind(this));
     },
 
-    _computeDraftsURL: function(changeNum) {
-      return '/changes/' + changeNum + '/drafts';
-    },
-
     _computeHideDraftList: function(drafts) {
       return Object.keys(drafts || {}).length == 0;
     },
@@ -125,7 +117,6 @@
 
     _cancelTapHandler: function(e) {
       e.preventDefault();
-      this._drafts = null;
       this.fire('cancel', null, {bubbles: false});
     },
 
@@ -149,7 +140,6 @@
         this.fire('send', null, {bubbles: false});
         this.draft = '';
         this.disabled = false;
-        this._drafts = null;
       }.bind(this)).catch(function(err) {
         alert('Oops. Something went wrong. Check the console and bug the ' +
             'PolyGerrit team for assistance.');
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 3cde22a..d3072ac 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -20,7 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 0d549d9..814e206 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -17,6 +17,7 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-account-dropdown">
   <style>
@@ -36,6 +37,11 @@
       font: inherit;
       padding: .3em 0;
     }
+    gr-avatar {
+      height: 2em;
+      width: 2em;
+      vertical-align: -.25em;
+    }
     ul {
       list-style: none;
     }
@@ -59,7 +65,11 @@
   </style>
   <template>
     <gr-button link class="dropdown-trigger" id="trigger"
-        on-tap="_showDropdownTapHandler">[[account.name]]</gr-button>
+        on-tap="_showDropdownTapHandler">
+      <span hidden$="[[_hasAvatars]]" hidden>[[account.name]]</span>
+      <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden
+          image-size="56"></gr-avatar>
+    </gr-button>
     <iron-dropdown id="dropdown"
         vertical-align="top"
         vertical-offset="25"
@@ -77,6 +87,7 @@
         </ul>
       </div>
     </iron-dropdown>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-account-dropdown.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 09de6c1..62212a3 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -19,6 +19,13 @@
 
     properties: {
       account: Object,
+      _hasAvatars: Boolean,
+    },
+
+    attached: function() {
+      this.$.restAPI.getConfig().then(function(cfg) {
+        this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
+      }.bind(this));
     },
 
     _showDropdownTapHandler: function(e) {
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 847a641..557c213 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
@@ -122,6 +122,7 @@
           this._navToFile(this._fileList, 1);
           break;
         case 78:  // 'n'
+          e.preventDefault();
           if (e.shiftKey) {
             this.$.diff.scrollToNextCommentThread();
           } else {
@@ -129,6 +130,7 @@
           }
           break;
         case 80:  // 'p'
+          e.preventDefault();
           if (e.shiftKey) {
             this.$.diff.scrollToPreviousCommentThread();
           } else {
@@ -150,6 +152,7 @@
           }
           break;
         case 188:  // ','
+          e.preventDefault();
           this.$.diff.showDiffPreferences();
           break;
       }
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 4d0c6e5..188bc5d 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
@@ -21,7 +21,7 @@
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
 <script src="../../../bower_components/page/page.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
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 71a04b8..cac1f3e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -87,7 +87,6 @@
       },
       _savedPrefs: Object,
 
-      _diffRequestsPromise: Object,  // Used for testing.
       _diffPreferencesPromise: Object,  // Used for testing.
     },
 
@@ -157,7 +156,7 @@
           }.bind(this)));
         }
 
-        this._diffRequestsPromise = Promise.all(promises).then(function() {
+        return Promise.all(promises).then(function() {
           this._render();
           this._loading = false;
         }.bind(this)).catch(function(err) {
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 921d766..91f5f3b 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
@@ -20,7 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
@@ -162,86 +162,85 @@
         patchNum: 2,
       };
 
-      element.reload();
-      server.respond();
-
-      // Allow events to fire and the threads to render.
-      flush(function() {
-        var leftThreadEls =
-            Polymer.dom(element.$.leftDiff.root).querySelectorAll(
-                'gr-diff-comment-thread');
-        assert.equal(leftThreadEls.length, 1);
-        assert.equal(leftThreadEls[0].comments.length, 1);
-
-        var rightThreadEls =
-            Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                'gr-diff-comment-thread');
-        assert.equal(rightThreadEls.length, 1);
-        assert.equal(rightThreadEls[0].comments.length, 2);
-
-        var index = leftThreadEls[0].getAttribute('data-index');
-        var leftFillerEls =
-            Polymer.dom(element.$.leftDiff.root).querySelectorAll(
-                '.commentThread.filler[data-index="' + index + '"]');
-        assert.equal(leftFillerEls.length, 1);
-        var rightFillerEls =
-            Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                '[data-index="' + index + '"]');
-        assert.equal(rightFillerEls.length, 2);
-
-        for (var i = 0; i < rightFillerEls.length; i++) {
-          assert.isTrue(rightFillerEls[i].classList.contains('filler'));
-        }
-        var originalHeight = rightFillerEls[0].offsetHeight;
-        assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
-        assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
-        assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
-
-        // Create a comment on the opposite side of the first comment.
-        var rightLineEL = element.$.rightDiff.$$(
-              '.lineNum[data-index="' + (index - 1) + '"]');
-        assert.ok(rightLineEL);
-        MockInteractions.tap(rightLineEL);
+      element.reload().then(function() {
         flush(function() {
-          var newThreadEls =
-            Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                '[data-index="' + index + '"]');
-          assert.equal(newThreadEls.length, 2);
-          for (var i = 0; i < newThreadEls.length; i++) {
-            assert.isTrue(
-                newThreadEls[i].classList.contains('commentThread') ||
-                newThreadEls[i].tagName == 'GR-DIFF-COMMENT-THREAD');
+          var leftThreadEls =
+              Polymer.dom(element.$.leftDiff.root).querySelectorAll(
+                  'gr-diff-comment-thread');
+          assert.equal(leftThreadEls.length, 1);
+          assert.equal(leftThreadEls[0].comments.length, 1);
+
+          var rightThreadEls =
+              Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+                  'gr-diff-comment-thread');
+          assert.equal(rightThreadEls.length, 1);
+          assert.equal(rightThreadEls[0].comments.length, 2);
+
+          var index = leftThreadEls[0].getAttribute('data-index');
+          var leftFillerEls =
+              Polymer.dom(element.$.leftDiff.root).querySelectorAll(
+                  '.commentThread.filler[data-index="' + index + '"]');
+          assert.equal(leftFillerEls.length, 1);
+          var rightFillerEls =
+              Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+                  '[data-index="' + index + '"]');
+          assert.equal(rightFillerEls.length, 2);
+
+          for (var i = 0; i < rightFillerEls.length; i++) {
+            assert.isTrue(rightFillerEls[i].classList.contains('filler'));
           }
-          var newHeight = newThreadEls[0].offsetHeight;
-          assert.equal(newThreadEls[1].offsetHeight, newHeight);
-          assert.equal(leftFillerEls[0].offsetHeight, newHeight);
-          assert.equal(leftThreadEls[0].offsetHeight, newHeight);
+          var originalHeight = rightFillerEls[0].offsetHeight;
+          assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
+          assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
+          assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
 
-          // The editing mode height of the right comment will be greater than
-          // the non-editing mode height of the left comment.
-          assert.isAbove(newHeight, originalHeight);
-
-          // Discard the right thread and ensure the left comment heights are
-          // back to their original values.
-          newThreadEls[1].addEventListener('discard', function() {
-            rightFillerEls =
-                Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                    '[data-index="' + index + '"]');
-            assert.equal(rightFillerEls.length, 2);
-
-            for (var i = 0; i < rightFillerEls.length; i++) {
-              assert.isTrue(rightFillerEls[i].classList.contains('filler'));
+          // Create a comment on the opposite side of the first comment.
+          var rightLineEL = element.$.rightDiff.$$(
+                '.lineNum[data-index="' + (index - 1) + '"]');
+          assert.ok(rightLineEL);
+          MockInteractions.tap(rightLineEL);
+          flush(function() {
+            var newThreadEls =
+              Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+                  '[data-index="' + index + '"]');
+            assert.equal(newThreadEls.length, 2);
+            for (var i = 0; i < newThreadEls.length; i++) {
+              assert.isTrue(
+                  newThreadEls[i].classList.contains('commentThread') ||
+                  newThreadEls[i].tagName == 'GR-DIFF-COMMENT-THREAD');
             }
-            var originalHeight = rightFillerEls[0].offsetHeight;
-            assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
-            assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
-            assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
-            done();
+            var newHeight = newThreadEls[0].offsetHeight;
+            assert.equal(newThreadEls[1].offsetHeight, newHeight);
+            assert.equal(leftFillerEls[0].offsetHeight, newHeight);
+            assert.equal(leftThreadEls[0].offsetHeight, newHeight);
+
+            // The editing mode height of the right comment will be greater than
+            // the non-editing mode height of the left comment.
+            assert.isAbove(newHeight, originalHeight);
+
+            // Discard the right thread and ensure the left comment heights are
+            // back to their original values.
+            newThreadEls[1].addEventListener('discard', function() {
+              rightFillerEls =
+                  Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+                      '[data-index="' + index + '"]');
+              assert.equal(rightFillerEls.length, 2);
+
+              for (var i = 0; i < rightFillerEls.length; i++) {
+                assert.isTrue(rightFillerEls[i].classList.contains('filler'));
+              }
+              var originalHeight = rightFillerEls[0].offsetHeight;
+              assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
+              assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
+              assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
+              done();
+            });
+            var commentEl = newThreadEls[1].$$('gr-diff-comment');
+            commentEl.fire('discard', null, {bubbles: false});
           });
-          var commentEl = newThreadEls[1].$$('gr-diff-comment');
-          commentEl.fire('discard', null, {bubbles: false});
         });
       });
+      server.respond();
     });
 
     test('intraline normalization', function() {
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 67ab50f..b1c084e 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -26,7 +26,6 @@
 <link rel="import" href="./change/gr-change-view/gr-change-view.html">
 <link rel="import" href="./diff/gr-diff-view/gr-diff-view.html">
 
-<link rel="import" href="./shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="./shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
@@ -56,11 +55,6 @@
         color: #b71c1c;
       }
     </style>
-    <gr-ajax auto url="/config/server/info" last-response="{{config}}"></gr-ajax>
-    <gr-ajax auto url="/config/server/version" last-response="{{version}}"></gr-ajax>
-    <gr-ajax id="diffPreferencesXHR"
-        url="/accounts/self/preferences.diff"
-        last-response="{{_diffPreferences}}"></gr-ajax>
     <gr-main-header search-query="{{params.query}}"></gr-main-header>
     <main>
       <template is="dom-if" if="{{_showChangeListView}}" restamp="true">
@@ -68,18 +62,18 @@
             params="[[params]]"
             view-state="{{_viewState.changeListView}}"
             changes-per-page="[[_preferences.changes_per_page]]"
-            logged-in="[[_computeLoggedIn(account)]]"></gr-change-list-view>
+            logged-in="[[_computeLoggedIn(_account)]]"></gr-change-list-view>
       </template>
       <template is="dom-if" if="{{_showDashboardView}}" restamp="true">
         <gr-dashboard-view
-            account="[[account]]"
+            account="[[_account]]"
             params="[[params]]"
             view-state="{{_viewState.dashboardView}}"></gr-dashboard-view>
       </template>
       <template is="dom-if" if="{{_showChangeView}}" restamp="true">
         <gr-change-view
             params="[[params]]"
-            server-config="[[config]]"
+            server-config="[[_serverConfig]]"
             view-state="{{_viewState.changeView}}"></gr-change-view>
       </template>
       <template is="dom-if" if="{{_showDiffView}}" restamp="true">
@@ -91,14 +85,14 @@
     </main>
     <footer role="contentinfo">
       Powered by <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a>
-      ([[version]])
-      <span hidden$="[[!config.gerrit.report_bug_url]]">
+      ([[_version]])
+      <span hidden$="[[!_serverConfig.gerrit.report_bug_url]]">
         |
-        <a href$="[[config.gerrit.report_bug_url]]" target="_blank">
-          <span hidden$="[[!config.gerrit.report_bug_text]]">
-            [[config.gerrit.report_bug_text]]
+        <a href$="[[_serverConfig.gerrit.report_bug_url]]" target="_blank">
+          <span hidden$="[[!_serverConfig.gerrit.report_bug_text]]">
+            [[_serverConfig.gerrit.report_bug_text]]
           </span>
-          <span hidden$="[[config.gerrit.report_bug_text]]">Report Bug</span>
+          <span hidden$="[[_serverConfig.gerrit.report_bug_text]]">Report Bug</span>
         </a>
       </span>
       |
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index a40e61b..55d3920 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -18,10 +18,7 @@
     is: 'gr-app',
 
     properties: {
-      account: {
-        type: Object,
-        observer: '_accountChanged',
-      },
+      params: Object,
       accountReady: {
         type: Object,
         readOnly: true,
@@ -32,29 +29,20 @@
           }.bind(this));
         },
       },
-      config: {
-        type: Object,
-        observer: '_configChanged',
-      },
-      configReady: {
-        type: Object,
-        readOnly: true,
-        notify: true,
-        value: function() {
-          return new Promise(function(resolve) {
-            this._resolveConfigReady = resolve;
-          }.bind(this));
-        },
-      },
-      version: String,
-      params: Object,
       keyEventTarget: {
         type: Object,
         value: function() { return document.body; },
       },
 
+      _account: {
+        type: Object,
+        observer: '_accountChanged',
+      },
+      _serverConfig: Object,
+      _version: String,
       _diffPreferences: Object,
       _preferences: Object,
+      _resolveAccountReady: Function,
       _showChangeListView: Boolean,
       _showDashboardView: Boolean,
       _showChangeView: Boolean,
@@ -75,12 +63,18 @@
     ],
 
     get loggedIn() {
-      return !!(this.account && Object.keys(this.account).length > 0);
+      return !!(this._account && Object.keys(this._account).length > 0);
     },
 
     attached: function() {
       this.$.restAPI.getAccount().then(function(account) {
-        this.account = account;
+        this._account = account;
+      }.bind(this));
+      this.$.restAPI.getConfig().then(function(config) {
+        this._serverConfig = config;
+      }.bind(this));
+      this.$.restAPI.getVersion().then(function(version) {
+        this._version = version;
       }.bind(this));
     },
 
@@ -103,14 +97,16 @@
       };
     },
 
-    _accountChanged: function() {
-      this._resolveAccountReady();
-      if (this.loggedIn) {
-        this.$.diffPreferencesXHR.generateRequest();
+    _accountChanged: function(account) {
+      this._resolveAccountReady(account);
 
+      if (this.loggedIn) {
         this.$.restAPI.getPreferences().then(function(preferences) {
           this._preferences = preferences;
         }.bind(this));
+        this.$.restAPI.getDiffPreferences().then(function(prefs) {
+          this._diffPreferences = prefs;
+        }.bind(this));
       } else {
         // These defaults should match the defaults in
         // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -137,10 +133,6 @@
       }
     },
 
-    _configChanged: function(config) {
-      this._resolveConfigReady(config);
-    },
-
     _viewChanged: function(view) {
       this.set('_showChangeListView', view == 'gr-change-list-view');
       this.set('_showDashboardView', view == 'gr-dashboard-view');
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index c39f288..af65bfd 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -20,7 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-account-label.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index e1ef862..869d812 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -20,7 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-account-link.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
index 3491443..55655c0 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -15,6 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-avatar">
   <template>
@@ -26,6 +27,7 @@
         background-color: var(--background-color, #f1f2f3);
       }
     </style>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-avatar.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 8f289ca..3655975 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -32,8 +32,8 @@
       this.hidden = true;
     },
 
-    ready: function() {
-      app.configReady.then(function(cfg) {
+    attached: function() {
+      this.$.restAPI.getConfig().then(function(cfg) {
         var hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
         if (hasAvatars) {
           this.hidden = false;
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index 7e3c25c..f065290 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -20,7 +20,7 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 
 <link rel="import" href="gr-avatar.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index 86ee947..03b8e13 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -21,7 +21,7 @@
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
 <script src="../../../bower_components/page/page.js"></script>
-<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
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 09b6d35..85d26e6 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
@@ -82,10 +82,28 @@
       }.bind(this));
     },
 
+    getConfig: function() {
+      return this._fetchSharedCacheURL('/config/server/info');
+    },
+
+    getVersion: function() {
+      return this._fetchSharedCacheURL('/config/server/version');
+    },
+
+    getDiffPreferences: function() {
+      return this._fetchSharedCacheURL('/accounts/self/preferences.diff');
+    },
+
     getAccount: function() {
       return this._fetchSharedCacheURL('/accounts/self/detail');
     },
 
+    getLoggedIn: function() {
+      return this.getAccount().then(function(account) {
+        return account != null;
+      });
+    },
+
     getPreferences: function() {
       return this._fetchSharedCacheURL('/accounts/self/preferences');
     },
@@ -112,6 +130,48 @@
       return this._sharedFetchPromises[url];
     },
 
+    getChangeFiles: function(changeNum, patchNum) {
+      return this.fetchJSON(
+          this._changeBaseURL(changeNum, patchNum) + '/files');
+    },
+
+    getReviewedFiles: function(changeNum, patchNum) {
+      return this.fetchJSON(
+          this._changeBaseURL(changeNum, patchNum) + '/files?reviewed');
+    },
+
+    saveFileReviewed: function(changeNum, patchNum, path, reviewed, opt_errFn,
+        opt_ctx) {
+      var method = reviewed ? 'PUT' : 'DELETE';
+      var url = this._changeBaseURL(changeNum, patchNum) + '/files/' +
+          encodeURIComponent(path) + '/reviewed';
+
+      return this._save(method, url, null, opt_errFn, opt_ctx);
+    },
+
+    _save: function(method, url, opt_body, opt_errFn, opt_ctx) {
+      var headers = new Headers({
+        'X-Gerrit-Auth': this._getCookie('XSRF_TOKEN'),
+      });
+
+      if (opt_body) {
+        headers.append('Content-Type', 'application/json');
+        options.body = body;
+      }
+      var options = {
+        method: method,
+        headers: headers,
+        credentials: 'same-origin',
+      };
+      return fetch(url, options).catch(function(err) {
+        if (opt_errFn) {
+          opt_errFn.call(opt_ctx || this);
+        } else {
+          throw err;
+        }
+      });
+    },
+
     getDiff: function(changeNum, basePatchNum, patchNum, path,
         opt_cancelCondition) {
       var url = this._getDiffFetchURL(changeNum, patchNum, path);
@@ -131,38 +191,46 @@
           encodeURIComponent(path) + '/diff';
     },
 
-    getDiffComments: function(changeNum, basePatchNum, patchNum, path) {
-      return this._getDiffComments(changeNum, basePatchNum, patchNum, path,
-          '/comments');
+    getDiffComments: function(changeNum, opt_basePatchNum, opt_patchNum,
+        opt_path) {
+      return this._getDiffComments(changeNum, '/comments', opt_basePatchNum,
+          opt_patchNum, opt_path);
     },
 
-    getDiffDrafts: function(changeNum, basePatchNum, patchNum, path) {
-      return this._getDiffComments(changeNum, basePatchNum, patchNum, path,
-          '/drafts');
+    getDiffDrafts: function(changeNum, opt_basePatchNum, opt_patchNum,
+        opt_path) {
+      return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum,
+          opt_patchNum, opt_path);
     },
 
-    _getDiffComments: function(changeNum, basePatchNum, patchNum, path,
-        endpoint) {
+    _getDiffComments: function(changeNum, endpoint, opt_basePatchNum,
+        opt_patchNum, opt_path) {
+      if (!opt_basePatchNum && !opt_patchNum && !opt_path) {
+        return this.fetchJSON(
+            this._getDiffCommentsFetchURL(changeNum, '/drafts'));
+      }
+
       function onlyParent(c) { return c.side == PARENT_PATCH_NUM; }
       function withoutParent(c) { return c.side != PARENT_PATCH_NUM; }
 
       var promises = [];
       var comments;
       var baseComments;
-      var url = this._getDiffCommentsFetchURL(changeNum, patchNum, endpoint);
+      var url =
+          this._getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum);
       promises.push(this.fetchJSON(url).then(function(response) {
-        comments = response[path] || [];
-        if (basePatchNum == PARENT_PATCH_NUM) {
+        comments = response[opt_path] || [];
+        if (opt_basePatchNum == PARENT_PATCH_NUM) {
           baseComments = comments.filter(onlyParent);
         }
         comments = comments.filter(withoutParent);
       }.bind(this)));
 
-      if (basePatchNum != PARENT_PATCH_NUM) {
-        var baseURL = this._getDiffCommentsFetchURL(changeNum, basePatchNum,
-            endpoint);
+      if (opt_basePatchNum != PARENT_PATCH_NUM) {
+        var baseURL = this._getDiffCommentsFetchURL(changeNum, endpoint,
+            opt_basePatchNum);
         promises.push(this.fetchJSON(baseURL).then(function(response) {
-          baseComments = (response[path] || []).filter(withoutParent);
+          baseComments = (response[opt_path] || []).filter(withoutParent);
         }));
       }
 
@@ -174,17 +242,32 @@
       });
     },
 
-    _getDiffCommentsFetchURL: function(changeNum, patchNum, endpoint) {
-      return this._changeBaseURL(changeNum, patchNum) + endpoint;
+    _getDiffCommentsFetchURL: function(changeNum, endpoint, opt_patchNum) {
+      return this._changeBaseURL(changeNum, opt_patchNum) + endpoint;
     },
 
-    _changeBaseURL: function(changeNum, patchNum) {
+    _changeBaseURL: function(changeNum, opt_patchNum) {
       var v = '/changes/' + changeNum;
-      if (patchNum) {
-        v += '/revisions/' + patchNum;
+      if (opt_patchNum) {
+        v += '/revisions/' + opt_patchNum;
       }
       return v;
     },
 
+    _getCookie: function(name) {
+      var key = name + '=';
+      var cookies = document.cookie.split(';');
+      for (var i = 0; i < cookies.length; i++) {
+        var c = cookies[i];
+        while (c.charAt(0) == ' ') {
+          c = c.substring(1);
+        }
+        if (c.indexOf(key) == 0) {
+          return c.substring(key.length, c.length);
+        }
+      }
+      return '';
+    },
+
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index b0a6649..44e49cb 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -42,7 +42,7 @@
       var testJSON = ')]}\'\n{"hello": "bonjour"}';
 
       var fetchStub = sinon.stub(window, 'fetch', function() {
-        return Promise.resolve({ text: function() {
+        return Promise.resolve({text: function() {
           return Promise.resolve(testJSON);
         }});
       });
@@ -65,7 +65,7 @@
 
       Promise.all(promises).then(function(results) {
         assert.deepEqual(results, [1, 1, 1]);
-         element._fetchSharedCacheURL('/foo').then(function(foo) {
+        element._fetchSharedCacheURL('/foo').then(function(foo) {
           assert.equal(foo, 1);
           fetchJSONStub.restore();
           done();
@@ -84,7 +84,7 @@
 
     test('params are properly encoded', function() {
       var fetchStub = sinon.stub(window, 'fetch', function() {
-        return Promise.resolve({ text: function() {
+        return Promise.resolve({text: function() {
           return Promise.resolve(')]}\'\n{}');
         }});
       });
@@ -101,7 +101,7 @@
     test('request callbacks can be canceled', function(done) {
       var cancelCalled = false;
       var fetchStub = sinon.stub(window, 'fetch', function() {
-        return Promise.resolve({ body: {
+        return Promise.resolve({body: {
           cancel: function() { cancelCalled = true; }
         }});
       });
@@ -129,7 +129,7 @@
           ],
         });
       });
-      element._getDiffComments('42', 'PARENT', 1, 'sieve.go', '').then(
+      element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then(
         function(obj) {
           assert.equal(obj.baseComments.length, 1);
           assert.deepEqual(obj.baseComments[0], {
@@ -140,6 +140,7 @@
           assert.deepEqual(obj.comments[0], {
             message: 'this isn’t quite right',
           });
+          fetchJSONStub.restore();
           done();
         });
     });
@@ -177,7 +178,7 @@
           });
         }
       });
-      element._getDiffComments('42', 1, 2, 'sieve.go', '').then(
+      element._getDiffComments('42', '', 1, 2, 'sieve.go').then(
         function(obj) {
           assert.equal(obj.baseComments.length, 1);
           assert.deepEqual(obj.baseComments[0], {
@@ -190,6 +191,7 @@
           assert.deepEqual(obj.comments[1], {
             message: '¯\\_(ツ)_/¯',
           });
+          fetchJSONStub.restore();
           done();
         });
     });
diff --git a/polygerrit-ui/app/scripts/fake-app.js b/polygerrit-ui/app/test/fake-app.js
similarity index 89%
rename from polygerrit-ui/app/scripts/fake-app.js
rename to polygerrit-ui/app/test/fake-app.js
index 87e2d04..5ee6915 100644
--- a/polygerrit-ui/app/scripts/fake-app.js
+++ b/polygerrit-ui/app/test/fake-app.js
@@ -19,10 +19,7 @@
  */
 var app = {
   accountReady: {
-    then: function(cb) { cb(); },
-  },
-  configReady: {
-    then: function(cb) { cb(); },
+    then: function(cb) { return cb(); },
   },
   loggedIn: false,
 };
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index 7017406..83a33e8 100755
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -65,7 +65,8 @@
       print(' '.join(exe), file=stderr)
     check_output(exe)
   except Exception as e:
-    print('%s command failed: %s' % (args.a, e), file=stderr)
+    print('%s command failed: %s\n%s' % (args.a, ' '.join(exe), e),
+      file=stderr)
     exit(1)
 
 with open(args.o, 'w') as fd:
diff --git a/tools/plugin_archetype_deploy.sh b/tools/plugin_archetype_deploy.sh
index 4ad8b70..b16ce95 100755
--- a/tools/plugin_archetype_deploy.sh
+++ b/tools/plugin_archetype_deploy.sh
@@ -63,21 +63,9 @@
     -Dfile=target/$module-$ver.jar
 }
 
-function confirm
-{
-  read -n1 -p "Are you sure you want to deploy? [N/y]: " ready
-  if [[ ! $ready == [Yy] ]]; then
-    if [[ $ready == [Nn] || -z $ready ]]; then
-      echo; exit
-    else
-      echo; confirm
-    fi
-  fi
-}
-
 function run
 {
-  test ${dryRun:-'false'} == 'false'  && confirm
+  test ${dryRun:-'false'} == 'false'
   root=$(instroot)
   cd "$root"
   ver=$(getver GERRIT_VERSION)