Merge "Fix emailStrategy ListBox not displayed on MyPreferencesScreen"
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index ef653cc..90212fb 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -120,7 +120,7 @@
 link:cmd-gsql.html[gerrit gsql]::
 	Administrative interface to active database.
 
-link:cmd-index-index.html[gerrit index activate]::
+link:cmd-index-activate.html[gerrit index activate]::
 	Activate the latest index version available.
 
 link:cmd-index-start.html[gerrit index start]::
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ce9b1b3..a38f9fc 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1394,21 +1394,37 @@
 used to automatically create correct database.driver and database.url
 values to open the connection.
 +
-* `POSTGRESQL`
+* `DB2`
 +
-Connect to a PostgreSQL database server.
+Connect to a DB2 database server.
++
+* `DERBY`
++
+Connect to an Apache Derby database server.
 +
 * `H2`
 +
 Connect to a local embedded H2 database.
 +
+* `JDBC`
++
+Connect using a JDBC driver class name and URL.
++
+* `MAXDB`
++
+Connect to an SAP MaxDb database server.
++
 * `MYSQL`
 +
 Connect to a MySQL database server.
 +
-* `JDBC`
+* `ORACLE`
 +
-Connect using a JDBC driver class name and URL.
+Connect to an Oracle database server.
++
+* `POSTGRESQL`
++
+Connect to a PostgreSQL database server.
 
 +
 If not specified, database.driver and database.url are used as-is,
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index e32e30d..6b26336 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -433,6 +433,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/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
new file mode 100644
index 0000000..f49de7d
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.12.1.txt
@@ -0,0 +1,236 @@
+Release notes for Gerrit 2.12.1
+===============================
+
+Gerrit 2.12.1 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.1.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.12.1.war]
+
+Gerrit 2.12.1 includes the bug fixes done with
+link:ReleaseNotes-2.11.6.html[Gerrit 2.11.6] and
+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.
+
+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;
+----
+
+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.
+
+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.
++
+The column type `VARCHAR(255)` was too small, preventing some PGP push
+certificates from being stored.
+
+* Add the `DRAFT_COMMENTS` option to the list changes REST API endpoint
+and mark it as deprecated.
++
+It was removed in version 2.12 because it's not needed any more by the UI,
+but this caused failures for clients that still use it.
++
+Now it is added back, although it does not do anything and is marked as
+deprecated.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3669[Issue 3669]:
+Fix schema migration when migrating to 2.12.x directly from a version
+earlier than 2.11.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3733[Issue 3733]:
+Correctly detect symlinked log directory on startup.
++
+If `$site_path/logs` was a symlink, the server would not start.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3871[Issue 3871]:
+Throw an explicit exception when failing to load a change from the database.
++
+If a change could not be loaded from the database, for example if it was
+manually removed from the changes table but references to it were remaining
+in other tables, a null change was returned which would then lead to an
+'Internal Server Error' that was difficult to track down. Now an error is
+raised earlier which will help administrators to find the root cause.
+
+* 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.
++
+When submitting a change that required rebase, the committer was being
+set to 'Gerrit Code Review' instead of the name of the submitter.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3758[Issue 3758]:
+Fix serving of static resources when deployed in application container.
++
+When deployed in a container, for example Tomcat, it was not possible to
+load the UI because static content could not be loaded from the WAR file.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3790[Issue 3790]:
+When deployed in a container, for example Tomcat, the 'Documentation' menu
+was missing.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3786[Issue 3786]:
+Fix SQL statement syntax in schema migration.
++
+An extra semicolon was preventing migration from 2.11.x to 2.12 when using
+an Oracle database.
+
+* Send email using email queue instead of the default queue.
++
+Some emails sent asynchronously were already being sent using that queue
+but some were not. This was confusing for a gerrit administrator because
+if there is a build up of `send-email` tasks in the queue, he would
+think that increasing `sendemail.threadPoolSize` would help but it did not
+because some of the email were sent using the default queue which is
+configurable using `execution.defaultThreadPoolSize`.
+
+* Fix XSRF token cookie to honor `auth.cookieSecure` setting.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3767[Issue 3767]:
+Fix replication of first patch set for new changes.
++
+When new changes were pushed from the command line, the first patch
+set did not get replicated to destinations.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3771[Issue 3771]:
+Remove `index.defaultMaxClauseCount` configuration option.
++
+When `index.maxTerms` was either not set (thus no limit) or set to a value
+higher than `index.defaultMaxClauseCount` it was possible that viewing the
+related changes tab could cause a 'Too many clauses' error for changes that
+have a lot of related changes.
++
+The `index.defaultMaxClauseCount` configuration option is removed, and the
+existing `index.maxTerms` is reused. The default value of `index.maxTerms`
+is reduced from 'no limit' to 1024.
+
+* 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.
+
+* 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.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3718[Issue 3718]:
+Hide avatar images when no avatars are available.
++
+The UI was showing a transparent empty image with a border.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3731[Issue 3731]:
+Fix syntax higlighting of tcl files.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3863[Issue 3863]:
+Fix display of active row marker in tag list.
++
+Clicking on one of the rows would cause the tag name to disappear.
+
+* 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.
+
+* 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
+~~~~~~~
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3821[Issue 3821]:
+Fix repeated reloading of plugins when running on OpenJDK 8.
++
+OpenJDK 8 uses nanotime precision for file modification time on systems that
+are POSIX 2008 compatible. This leads to precision incompatibility when
+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
+--------
+
+* Upgrade JGit to 4.1.2.201602141800-r.
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 fa57dab..4cab151 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -9,11 +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/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 35e86c5..291b953 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -43,6 +43,8 @@
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 
+import java.util.List;
+
 public class PushOneCommit {
   public static final String SUBJECT = "test commit";
   public static final String FILE_NAME = "a.txt";
@@ -179,6 +181,13 @@
       .committer(new PersonIdent(i, testRepo.getDate()));
   }
 
+  public void setParents(List<RevCommit> parents) throws Exception {
+    commitBuilder.noParents();
+    for (RevCommit p : parents) {
+      commitBuilder.parent(p);
+    }
+  }
+
   public Result to(String ref) throws Exception {
     commitBuilder.add(fileName, content);
     return execute(ref);
@@ -189,7 +198,7 @@
     return execute(ref);
   }
 
-  private Result execute(String ref) throws Exception {
+  public Result execute(String ref) throws Exception {
     RevCommit c = commitBuilder.create();
     if (changeId == null) {
       changeId = GitUtil.getChangeId(testRepo, c).get();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 7e107cf..431cfaa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -189,7 +189,7 @@
         .setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master"))
         .call();
     assertCommit(project, "refs/heads/master");
-    assertThat(getSubmitter(r.getPatchSetId())).isNull();
+    assertSubmitApproval(r.getPatchSetId());
     ChangeInfo c =
         gApi.changes().id(r.getPatchSetId().getParentKey().get()).get();
     assertThat(c.status).isEqualTo(ChangeStatus.MERGED);
@@ -209,7 +209,7 @@
     r.assertOkStatus();
 
     assertCommit(project, "refs/heads/master");
-    assertThat(getSubmitter(r.getPatchSetId())).isNull();
+    assertSubmitApproval(r.getPatchSetId());
     ChangeInfo c =
         gApi.changes().id(r.getPatchSetId().getParentKey().get()).get();
     assertThat(c.status).isEqualTo(ChangeStatus.MERGED);
@@ -225,7 +225,7 @@
 
   private void assertSubmitApproval(PatchSet.Id patchSetId) throws Exception {
     PatchSetApproval a = getSubmitter(patchSetId);
-    assertThat(a.isSubmit()).isTrue();
+    assertThat(a.isLegacySubmit()).isTrue();
     assertThat(a.getValue()).isEqualTo((short) 1);
     assertThat(a.getAccountId()).isEqualTo(admin.id);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 2b7f930..28a01fb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -356,7 +356,7 @@
     PatchSetApproval submitter = approvalsUtil.getSubmitter(
         db, cn, new PatchSet.Id(cn.getChangeId(), psId));
     assertThat(submitter).isNotNull();
-    assertThat(submitter.isSubmit()).isTrue();
+    assertThat(submitter.isLegacySubmit()).isTrue();
     assertThat(submitter.getAccountId()).isEqualTo(admin.getId());
   }
 
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 d6c8dac..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(
-          "Clicking the button would fail for other changes");
+          "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
new file mode 100644
index 0000000..5a6c36a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -0,0 +1,363 @@
+// 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.common.truth.TruthJUnit.assume;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.git.ChangeSet;
+import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@NoHttpd
+public class SubmitResolvingMergeCommitIT extends AbstractDaemonTest {
+  @Inject
+  private MergeSuperSet mergeSuperSet;
+
+  @Inject
+  private Submit submit;
+
+  @ConfigSuite.Default
+  public static Config submitWholeTopicEnabled() {
+    return submitWholeTopicEnabledConfig();
+  }
+
+  @Test
+  public void resolvingMergeCommitAtEndOfChain() throws Exception {
+    /*
+      A <- B <- C <------- D
+      ^                    ^
+      |                    |
+      E <- F <- G <- H <-- M*
+
+      G has a conflict with C and is resolved in M which is a merge
+      commit of H and D.
+    */
+
+    PushOneCommit.Result a = createChange("A");
+    PushOneCommit.Result b = createChange("B", "new.txt", "No conflict line",
+        ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result c = createChange("C", ImmutableList.of(b.getCommit()));
+    PushOneCommit.Result d = createChange("D", ImmutableList.of(c.getCommit()));
+
+    PushOneCommit.Result e = createChange("E", ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result f = createChange("F", ImmutableList.of(e.getCommit()));
+    PushOneCommit.Result g = createChange("G", "new.txt", "Conflicting line",
+        ImmutableList.of(f.getCommit()));
+    PushOneCommit.Result h = createChange("H", ImmutableList.of(g.getCommit()));
+
+    approve(a.getChangeId());
+    approve(b.getChangeId());
+    approve(c.getChangeId());
+    approve(d.getChangeId());
+    submit(d.getChangeId());
+
+    approve(e.getChangeId());
+    approve(f.getChangeId());
+    approve(g.getChangeId());
+    approve(h.getChangeId());
+
+    assertMergeable(e.getChange(), true);
+    assertMergeable(f.getChange(), true);
+    assertMergeable(g.getChange(), false);
+    assertMergeable(h.getChange(), false);
+
+    PushOneCommit.Result m = createChange("M", "new.txt", "Resolved conflict",
+        ImmutableList.of(d.getCommit(), h.getCommit()));
+    approve(m.getChangeId());
+
+    assertChangeSetMergeable(m.getChange(), true);
+
+    assertMergeable(m.getChange(), true);
+    submit(m.getChangeId());
+
+    assertMerged(e.getChangeId());
+    assertMerged(f.getChangeId());
+    assertMerged(g.getChangeId());
+    assertMerged(h.getChangeId());
+    assertMerged(m.getChangeId());
+  }
+
+  @Test
+  public void resolvingMergeCommitComingBeforeConflict() throws Exception {
+    /*
+      A <- B <- C <- D
+      ^    ^
+      |    |
+      E <- F* <- G
+
+      F is a merge commit of E and B and resolves any conflict.
+      However G is conflicting with C.
+    */
+
+    PushOneCommit.Result a = createChange("A");
+    PushOneCommit.Result b = createChange("B", "new.txt", "No conflict line",
+        ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result c = createChange("C", "new.txt", "No conflict line #2",
+        ImmutableList.of(b.getCommit()));
+    PushOneCommit.Result d = createChange("D", ImmutableList.of(c.getCommit()));
+    PushOneCommit.Result e = createChange("E", "new.txt", "Conflicting line",
+        ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result f = createChange("F", "new.txt", "Resolved conflict",
+        ImmutableList.of(b.getCommit(), e.getCommit()));
+    PushOneCommit.Result g = createChange("G", "new.txt", "Conflicting line #2",
+        ImmutableList.of(f.getCommit()));
+
+    assertMergeable(e.getChange(), true);
+
+    approve(a.getChangeId());
+    approve(b.getChangeId());
+    submit(b.getChangeId());
+
+    assertMergeable(e.getChange(), false);
+    assertMergeable(f.getChange(), true);
+    assertMergeable(g.getChange(), true);
+
+    approve(c.getChangeId());
+    approve(d.getChangeId());
+    submit(d.getChangeId());
+
+    approve(e.getChangeId());
+    approve(f.getChangeId());
+    approve(g.getChangeId());
+
+    assertMergeable(g.getChange(), false);
+    assertChangeSetMergeable(g.getChange(), false);
+  }
+
+  @Test
+  public void resolvingMergeCommitWithTopics() throws Exception {
+    /*
+      Project1:
+        A <- B <-- C <---
+        ^    ^          |
+        |    |          |
+        E <- F* <- G <- L*
+
+      G clashes with C, and F resolves the clashes between E and B.
+      Later, L resolves the clashes between C and G.
+
+      Project2:
+        H <- I
+        ^    ^
+        |    |
+        J <- K*
+
+      J clashes with I, and K resolves all problems.
+      G, K and L are in the same topic.
+    */
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+
+    String project1Name = name("Project1");
+    String project2Name = name("Project2");
+    gApi.projects().create(project1Name);
+    gApi.projects().create(project2Name);
+    TestRepository<InMemoryRepository> project1 =
+        cloneProject(new Project.NameKey(project1Name));
+    TestRepository<InMemoryRepository> project2 =
+        cloneProject(new Project.NameKey(project2Name));
+
+    PushOneCommit.Result a = createChange(project1, "A");
+    PushOneCommit.Result b = createChange(project1, "B", "new.txt",
+        "No conflict line", ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result c = createChange(project1, "C", "new.txt",
+        "No conflict line #2", ImmutableList.of(b.getCommit()));
+
+    approve(a.getChangeId());
+    approve(b.getChangeId());
+    approve(c.getChangeId());
+    submit(c.getChangeId());
+
+    PushOneCommit.Result e = createChange(project1, "E", "new.txt",
+        "Conflicting line", ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result f = createChange(project1, "F", "new.txt",
+        "Resolved conflict", ImmutableList.of(b.getCommit(), e.getCommit()));
+    PushOneCommit.Result g = createChange(project1, "G", "new.txt",
+        "Conflicting line #2", ImmutableList.of(f.getCommit()),
+        "refs/for/master/" + name("topic1"));
+
+    PushOneCommit.Result h = createChange(project2, "H");
+    PushOneCommit.Result i = createChange(project2, "I", "new.txt",
+        "No conflict line", ImmutableList.of(h.getCommit()));
+    PushOneCommit.Result j = createChange(project2, "J", "new.txt",
+        "Conflicting line", ImmutableList.of(h.getCommit()));
+    PushOneCommit.Result k =
+        createChange(project2, "K", "new.txt", "Sadly conflicting topic-wise",
+            ImmutableList.of(i.getCommit(), j.getCommit()),
+            "refs/for/master/" + name("topic1"));
+
+    approve(h.getChangeId());
+    approve(i.getChangeId());
+    submit(i.getChangeId());
+
+    approve(e.getChangeId());
+    approve(f.getChangeId());
+    approve(g.getChangeId());
+    approve(j.getChangeId());
+    approve(k.getChangeId());
+
+    assertChangeSetMergeable(g.getChange(), false);
+    assertChangeSetMergeable(k.getChange(), false);
+
+    PushOneCommit.Result l =
+        createChange(project1, "L", "new.txt", "Resolving conflicts again",
+            ImmutableList.of(c.getCommit(), g.getCommit()),
+            "refs/for/master/" + name("topic1"));
+
+    approve(l.getChangeId());
+    assertChangeSetMergeable(l.getChange(), true);
+
+    submit(l.getChangeId());
+    assertMerged(c.getChangeId());
+    assertMerged(g.getChangeId());
+    assertMerged(k.getChangeId());
+  }
+
+  @Test
+  public void resolvingMergeCommitAtEndOfChainAndNotUpToDate() throws Exception {
+    /*
+        A <-- B
+         \
+          C  <- D
+           \   /
+             E
+
+        B is the target branch, and D should be merged with B, but one
+        of C conflicts with B
+    */
+
+    PushOneCommit.Result a = createChange("A");
+    PushOneCommit.Result b = createChange("B", "new.txt", "No conflict line",
+        ImmutableList.of(a.getCommit()));
+
+    approve(a.getChangeId());
+    approve(b.getChangeId());
+    submit(b.getChangeId());
+
+    PushOneCommit.Result c = createChange("C", "new.txt", "Create conflicts",
+        ImmutableList.of(a.getCommit()));
+    PushOneCommit.Result e = createChange("E", ImmutableList.of(c.getCommit()));
+    PushOneCommit.Result d = createChange("D", "new.txt", "Resolves conflicts",
+        ImmutableList.of(c.getCommit(), e.getCommit()));
+
+    approve(c.getChangeId());
+    approve(e.getChangeId());
+    approve(d.getChangeId());
+    assertMergeable(d.getChange(), false);
+    assertChangeSetMergeable(d.getChange(), false);
+  }
+
+  private void submit(String changeId) throws Exception {
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .submit();
+  }
+
+  private void assertChangeSetMergeable(ChangeData change, boolean expected)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException,
+      OrmException {
+    ChangeSet cs =
+        mergeSuperSet.completeChangeSet(db, change.change(), user(admin));
+    assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
+  }
+
+  private void assertMergeable(ChangeData change, boolean expected)
+      throws Exception {
+    change.setMergeable(null);
+    assertThat(change.isMergeable()).isEqualTo(expected);
+  }
+
+  private void assertMerged(String changeId) throws Exception {
+    assertThat(gApi
+        .changes()
+        .id(changeId)
+        .get()
+        .status).isEqualTo(ChangeStatus.MERGED);
+  }
+
+  private PushOneCommit.Result createChange(TestRepository<?> repo,
+      String subject, String fileName, String content, List<RevCommit> parents,
+      String ref) throws Exception {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo,
+        subject, fileName, content);
+
+    if (!parents.isEmpty()) {
+      push.setParents(parents);
+    }
+
+    PushOneCommit.Result result;
+    if (fileName.isEmpty()) {
+      result = push.execute(ref);
+    } else {
+      result = push.to(ref);
+    }
+    result.assertOkStatus();
+    return result;
+  }
+
+  private PushOneCommit.Result createChange(TestRepository<?> repo,
+      String subject) throws Exception {
+    return createChange(repo, subject, "x", "x", new ArrayList<RevCommit>(),
+        "refs/for/master");
+  }
+
+  private PushOneCommit.Result createChange(TestRepository<?> repo,
+      String subject, String fileName, String content, List<RevCommit> parents)
+          throws Exception {
+    return createChange(repo, subject, fileName, content, parents,
+        "refs/for/master");
+  }
+
+  @Override
+  protected PushOneCommit.Result createChange(String subject) throws Exception {
+    return createChange(testRepo, subject, "", "",
+        Collections.<RevCommit> emptyList(), "refs/for/master");
+  }
+
+  private PushOneCommit.Result createChange(String subject,
+      List<RevCommit> parents) throws Exception {
+    return createChange(testRepo, subject, "", "", parents, "refs/for/master");
+  }
+
+  private PushOneCommit.Result createChange(String subject, String fileName,
+      String content, List<RevCommit> parents) throws Exception {
+    return createChange(testRepo, subject, fileName, content, parents,
+        "refs/for/master");
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
index e09c63a..b77fb01 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.project.SetParent;
 
 import org.junit.Test;
@@ -49,6 +50,19 @@
         newGson().fromJson(r.getReader(), String.class);
     assertThat(newParent).isEqualTo(parent);
     r.consume();
+
+    // When the parent name is not explicitly set, it should be
+    // set to "All-Projects".
+    r = adminSession.put("/projects/" + project.get() + "/parent",
+          newParentInput(null));
+    r.assertOK();
+    r.consume();
+
+    r = adminSession.get("/projects/" + project.get() + "/parent");
+    r.assertOK();
+    newParent = newGson().fromJson(r.getReader(), String.class);
+    assertThat(newParent).isEqualTo(AllProjectsNameProvider.DEFAULT);
+    r.consume();
   }
 
   @Test
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..4d368f6 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,5 @@
 package com.google.gerrit.extensions.api.changes;
 
 public class SubmitInput {
-  @Deprecated
-  public boolean waitForMerge;
   public String onBehalfOf;
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
index e2fec27..2625222 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
@@ -136,15 +136,6 @@
     if (mask == 0) {
       mask = event.getNativeEvent().getKeyCode();
     }
-    if (event.isAltKeyDown()) {
-      mask |= KeyCommand.M_ALT;
-    }
-    if (event.isControlKeyDown()) {
-      mask |= KeyCommand.M_CTRL;
-    }
-    if (event.isMetaKeyDown()) {
-      mask |= KeyCommand.M_META;
-    }
     return mask;
   }
 }
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 c652cb2..2996c07 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
@@ -368,8 +368,8 @@
     KeyMap keyMap = KeyMap.create()
         .on("A", upToChange(true))
         .on("U", upToChange(false))
-        .on("[", header.navigate(Direction.PREV))
-        .on("]", header.navigate(Direction.NEXT))
+        .on("'['", header.navigate(Direction.PREV))
+        .on("']'", header.navigate(Direction.NEXT))
         .on("R", header.toggleReviewed())
         .on("O", commentManager.toggleOpenBox(cm))
         .on("Enter", commentManager.toggleOpenBox(cm))
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 190de6d..5bbb798 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
@@ -47,6 +47,7 @@
 import com.google.gerrit.server.notedb.ChangeRebuilder;
 import com.google.gerrit.server.notedb.NoteDbModule;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
@@ -221,13 +222,20 @@
     Multimap<Project.NameKey, Change.Id> changesByProject =
         ArrayListMultimap.create();
     try (ReviewDb db = schemaFactory.open()) {
-      for (Change c : db.changes().all()) {
+      for (Change c : unwrap(db).changes().all()) {
         changesByProject.put(c.getProject(), c.getId());
       }
       return changesByProject;
     }
   }
 
+  private static ReviewDb unwrap(ReviewDb db) {
+    if (db instanceof DisabledChangesReviewDbWrapper) {
+      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
+    }
+    return db;
+  }
+
   private static class RebuildListener implements Runnable {
     private Change.Id changeId;
     private ListenableFuture<?> future;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java
index f2af5fa..5239447 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java
@@ -20,7 +20,11 @@
 public class LabelId extends StringKey<com.google.gwtorm.client.Key<?>> {
   private static final long serialVersionUID = 1L;
 
-  public static final LabelId SUBMIT = new LabelId("SUBM");
+  static final String LEGACY_SUBMIT_NAME = "SUBM";
+
+  public static LabelId legacySubmit() {
+    return new LabelId(LEGACY_SUBMIT_NAME);
+  }
 
   @Column(id = 1)
   public String id;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index bc7ebc9..c89be30 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -149,8 +149,8 @@
     return getLabelId().get();
   }
 
-  public boolean isSubmit() {
-    return LabelId.SUBMIT.get().equals(getLabel());
+  public boolean isLegacySubmit() {
+    return LabelId.LEGACY_SUBMIT_NAME.equals(getLabel());
   }
 
   @Override
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..5442f07
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
@@ -0,0 +1,183 @@
+// 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).
+   */
+  private final DynamicSet<UserScopedEventListener> listeners;
+
+  /** Listeners to receive all changes as they happen. */
+  private final DynamicSet<EventListener> unrestrictedListeners;
+
+  private final ProjectCache projectCache;
+
+  private 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);
+  }
+
+  private 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);
+    }
+    return true;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 677f849..78cfb15 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -321,7 +321,7 @@
     }
     PatchSetApproval submitter = null;
     for (PatchSetApproval a : approvals) {
-      if (a.getPatchSetId().equals(c) && a.getValue() > 0 && a.isSubmit()) {
+      if (a.getPatchSetId().equals(c) && a.getValue() > 0 && a.isLegacySubmit()) {
         if (submitter == null
             || a.getGranted().compareTo(submitter.getGranted()) > 0) {
           submitter = a;
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..52446fe 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
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.change.SuggestChangeReviewers;
 import com.google.gerrit.server.change.Reviewed.DeleteReviewed;
 import com.google.gerrit.server.change.Reviewed.PutReviewed;
 
@@ -72,7 +73,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/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index bf40bbb..ef4f6d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -611,7 +611,7 @@
 
       for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
           ctx.getDb(), ctx.getControl(), psId, user.getAccountId())) {
-        if (a.isSubmit()) {
+        if (a.isLegacySubmit()) {
           continue;
         }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index f07a7ed..84f7307 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.auto.value.AutoValue;
-import com.google.common.primitives.Ints;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
@@ -31,16 +29,13 @@
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.change.RebaseUtil.Base;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -70,10 +65,7 @@
   private final RebaseChangeOp.Factory rebaseFactory;
   private final RebaseUtil rebaseUtil;
   private final ChangeJson.Factory json;
-  private final ChangeNotes.Factory notesFactory;
   private final Provider<ReviewDb> dbProvider;
-  private final Provider<InternalChangeQuery> queryProvider;
-  private final PatchSetUtil psUtil;
 
   @Inject
   public Rebase(BatchUpdate.Factory updateFactory,
@@ -81,19 +73,13 @@
       RebaseChangeOp.Factory rebaseFactory,
       RebaseUtil rebaseUtil,
       ChangeJson.Factory json,
-      ChangeNotes.Factory notesFactory,
-      Provider<ReviewDb> dbProvider,
-      Provider<InternalChangeQuery> queryProvider,
-      PatchSetUtil psUtil) {
+      Provider<ReviewDb> dbProvider) {
     this.updateFactory = updateFactory;
     this.repoManager = repoManager;
     this.rebaseFactory = rebaseFactory;
     this.rebaseUtil = rebaseUtil;
     this.json = json;
-    this.notesFactory = notesFactory;
     this.dbProvider = dbProvider;
-    this.queryProvider = queryProvider;
-    this.psUtil = psUtil;
   }
 
   @Override
@@ -144,7 +130,7 @@
 
     @SuppressWarnings("resource")
     ReviewDb db = dbProvider.get();
-    Base base = parseBase(rsrc, str);
+    Base base = rebaseUtil.parseBase(rsrc, str);
     if (base == null) {
       throw new ResourceConflictException("base revision is missing: " + str);
     }
@@ -180,72 +166,6 @@
     return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
   }
 
-  @AutoValue
-  static abstract class Base {
-    private static Base create(ChangeControl ctl, PatchSet ps) {
-      if (ctl == null) {
-        return null;
-      }
-      return new AutoValue_Rebase_Base(ctl, ps);
-    }
-
-    abstract ChangeControl control();
-    abstract PatchSet patchSet();
-  }
-
-  private Base parseBase(RevisionResource rsrc, String base)
-      throws OrmException, NoSuchChangeException {
-    ReviewDb db = dbProvider.get();
-
-    // Try parsing the base as a ref string.
-    PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
-    if (basePatchSetId != null) {
-      Change.Id baseChangeId = basePatchSetId.getParentKey();
-      ChangeControl baseCtl = controlFor(rsrc, baseChangeId);
-      if (baseCtl != null) {
-        return Base.create(
-            controlFor(rsrc, basePatchSetId.getParentKey()),
-            psUtil.get(db, baseCtl.getNotes(), basePatchSetId));
-      }
-    }
-
-    // Try parsing base as a change number (assume current patch set).
-    Integer baseChangeId = Ints.tryParse(base);
-    if (baseChangeId != null) {
-      ChangeControl baseCtl = controlFor(rsrc, new Change.Id(baseChangeId));
-      if (baseCtl != null) {
-        return Base.create(baseCtl, psUtil.current(db, baseCtl.getNotes()));
-      }
-    }
-
-    // Try parsing as SHA-1.
-    Base ret = null;
-    for (ChangeData cd : queryProvider.get()
-        .byProjectCommit(rsrc.getProject(), base)) {
-      for (PatchSet ps : cd.patchSets()) {
-        if (!ps.getRevision().matches(base)) {
-          continue;
-        }
-        if (ret == null || ret.patchSet().getId().get() < ps.getId().get()) {
-          ret = Base.create(
-              rsrc.getControl().getProjectControl().controlFor(cd.notes()),
-              ps);
-        }
-      }
-    }
-    return ret;
-  }
-
-  private ChangeControl controlFor(RevisionResource rsrc, Change.Id id)
-      throws OrmException, NoSuchChangeException {
-    if (rsrc.getChange().getId().equals(id)) {
-      return rsrc.getControl();
-    }
-    ChangeNotes notes =
-        notesFactory.createChecked(dbProvider.get(), rsrc.getProject(), id);
-    return rsrc.getControl().getProjectControl().controlFor(notes);
-  }
-
   private boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
     // Prevent rebase of exotic changes (merge commit, no ancestor).
     RevCommit c = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 4366019..c7e11d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.change.RebaseUtil.Base;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.BatchUpdate.Context;
@@ -31,6 +32,7 @@
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
@@ -114,7 +116,7 @@
   @Override
   public void updateRepo(RepoContext ctx) throws MergeConflictException,
        InvalidChangeOperationException, RestApiException, IOException,
-       OrmException {
+       OrmException, NoSuchChangeException {
     // Ok that originalPatchSet was not read in a transaction, since we just
     // need its revision.
     RevId oldRev = originalPatchSet.getRevision();
@@ -133,6 +135,12 @@
 
     rebasedCommit = rebaseCommit(ctx, original, baseCommit);
 
+    RevId baseRevId = new RevId((baseCommitish != null) ? baseCommitish
+        : ObjectId.toString(baseCommit.getId()));
+    Base base = rebaseUtil.parseBase(
+        new RevisionResource(new ChangeResource(ctl), originalPatchSet),
+        baseRevId.get());
+
     rebasedPatchSetId = ChangeUtil.nextPatchSetId(
         ctx.getRepository(), ctl.getChange().currentPatchSetId());
     patchSetInserter = patchSetInserterFactory
@@ -144,6 +152,10 @@
         .setMessage(
           "Patch Set " + rebasedPatchSetId.get()
           + ": Patch Set " + originalPatchSet.getId().get() + " was rebased");
+
+    if (base != null) {
+      patchSetInserter.setGroups(base.patchSet().getGroups());
+    }
     if (validate != null) {
       patchSetInserter.setValidatePolicy(validate);
     }
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 b75a659..dd9a0b9 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
@@ -14,14 +14,21 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.auto.value.AutoValue;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.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.server.PatchSetUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
@@ -43,10 +50,19 @@
   private static final Logger log = LoggerFactory.getLogger(RebaseUtil.class);
 
   private final Provider<InternalChangeQuery> queryProvider;
+  private final ChangeNotes.Factory notesFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final PatchSetUtil psUtil;
 
   @Inject
-  RebaseUtil(Provider<InternalChangeQuery> queryProvider) {
+  RebaseUtil(Provider<InternalChangeQuery> queryProvider,
+      ChangeNotes.Factory notesFactory,
+      Provider<ReviewDb> dbProvider,
+      PatchSetUtil psUtil) {
     this.queryProvider = queryProvider;
+    this.notesFactory = notesFactory;
+    this.dbProvider = dbProvider;
+    this.psUtil = psUtil;
   }
 
   public boolean canRebase(PatchSet patchSet, Branch.NameKey dest,
@@ -64,6 +80,72 @@
     }
   }
 
+  @AutoValue
+  static abstract class Base {
+    private static Base create(ChangeControl ctl, PatchSet ps) {
+      if (ctl == null) {
+        return null;
+      }
+      return new AutoValue_RebaseUtil_Base(ctl, ps);
+    }
+
+    abstract ChangeControl control();
+    abstract PatchSet patchSet();
+  }
+
+  Base parseBase(RevisionResource rsrc, String base)
+      throws OrmException, NoSuchChangeException {
+    ReviewDb db = dbProvider.get();
+
+    // Try parsing the base as a ref string.
+    PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
+    if (basePatchSetId != null) {
+      Change.Id baseChangeId = basePatchSetId.getParentKey();
+      ChangeControl baseCtl = controlFor(rsrc, baseChangeId);
+      if (baseCtl != null) {
+        return Base.create(
+            controlFor(rsrc, basePatchSetId.getParentKey()),
+            psUtil.get(db, baseCtl.getNotes(), basePatchSetId));
+      }
+    }
+
+    // Try parsing base as a change number (assume current patch set).
+    Integer baseChangeId = Ints.tryParse(base);
+    if (baseChangeId != null) {
+      ChangeControl baseCtl = controlFor(rsrc, new Change.Id(baseChangeId));
+      if (baseCtl != null) {
+        return Base.create(baseCtl, psUtil.current(db, baseCtl.getNotes()));
+      }
+    }
+
+    // Try parsing as SHA-1.
+    Base ret = null;
+    for (ChangeData cd : queryProvider.get()
+        .byProjectCommit(rsrc.getProject(), base)) {
+      for (PatchSet ps : cd.patchSets()) {
+        if (!ps.getRevision().matches(base)) {
+          continue;
+        }
+        if (ret == null || ret.patchSet().getId().get() < ps.getId().get()) {
+          ret = Base.create(
+              rsrc.getControl().getProjectControl().controlFor(cd.notes()),
+              ps);
+        }
+      }
+    }
+    return ret;
+  }
+
+  private ChangeControl controlFor(RevisionResource rsrc, Change.Id id)
+      throws OrmException, NoSuchChangeException {
+    if (rsrc.getChange().getId().equals(id)) {
+      return rsrc.getControl();
+    }
+    ChangeNotes notes =
+        notesFactory.createChecked(dbProvider.get(), rsrc.getProject(), id);
+    return rsrc.getControl().getProjectControl().controlFor(notes);
+  }
+
   /**
    * Find the commit onto which a patch set should be rebased.
    * <p>
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 154f8d9..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,16 @@
 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;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -29,9 +34,11 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -58,12 +65,19 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 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;
 
 @Singleton
 public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
@@ -85,8 +99,8 @@
       "This change depends on other hidden changes which are not ready";
   private static final String CLICK_FAILURE_TOOLTIP =
       "Clicking the button would fail";
-  private static final String CLICK_FAILURE_OTHER_TOOLTIP =
-      "Clicking the button would fail for other changes";
+  private static final String CHANGES_NOT_MERGEABLE =
+      "See the \"Submitted Together\" tab for problems, specially see: ";
 
   public static class Output {
     transient Change change;
@@ -104,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;
     }
   }
@@ -129,6 +141,7 @@
   private final ParameterizedString submitTopicTooltip;
   private final boolean submitWholeTopic;
   private final Provider<InternalChangeQuery> queryProvider;
+  private final PatchSetUtil psUtil;
 
   @Inject
   Submit(Provider<ReviewDb> dbProvider,
@@ -141,7 +154,8 @@
       AccountsCollection accounts,
       ChangesCollection changes,
       @GerritServerConfig Config cfg,
-      Provider<InternalChangeQuery> queryProvider) {
+      Provider<InternalChangeQuery> queryProvider,
+      PatchSetUtil psUtil) {
     this.dbProvider = dbProvider;
     this.repoManager = repoManager;
     this.changeDataFactory = changeDataFactory;
@@ -173,6 +187,7 @@
         cfg.getString("change", null, "submitTopicTooltip"),
         DEFAULT_TOPIC_TOOLTIP));
     this.queryProvider = queryProvider;
+    this.psUtil = psUtil;
   }
 
   @Override
@@ -247,24 +262,25 @@
         if (!changeControl.canSubmit()) {
           return BLOCKED_SUBMIT_TOOLTIP;
         }
-        // Recheck mergeability rather than using value stored in the index,
-        // which may be stale.
-        // TODO(dborowitz): This is ugly; consider providing a way to not read
-        // stored fields from the index in the first place.
-        c.setMergeable(null);
-        Boolean mergeable = c.isMergeable();
-        if (mergeable == null) {
-          log.error("Ephemeral error checking if change is submittable");
-          return CLICK_FAILURE_TOOLTIP;
-        }
-        if (!mergeable) {
-          return CLICK_FAILURE_OTHER_TOOLTIP;
-        }
         MergeOp.checkSubmitRule(c);
       }
+
+      Collection<ChangeData> unmergeable = unmergeableChanges(cs);
+      if (unmergeable == null) {
+        return CLICK_FAILURE_TOOLTIP;
+      } 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;
-    } catch (OrmException e) {
+    } catch (OrmException | IOException e) {
       log.error("Error checking if change is submittable", e);
       throw new OrmRuntimeException("Could not determine problems for the change", e);
     }
@@ -400,6 +416,71 @@
     return change != null ? change.getStatus().name().toLowerCase() : "deleted";
   }
 
+  public Collection<ChangeData> unmergeableChanges(ChangeSet cs)
+      throws OrmException, IOException {
+    Set<ChangeData> mergeabilityMap = new HashSet<>();
+    for (ChangeData change : cs.changes()) {
+      mergeabilityMap.add(change);
+    }
+
+    Multimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch();
+    for (Branch.NameKey branch : cbb.keySet()) {
+      Collection<ChangeData> targetBranch = cbb.get(branch);
+      HashMap<Change.Id, RevCommit> commits =
+          findCommits(targetBranch, branch.getParentKey());
+
+      Set<ObjectId> allParents = Sets.newHashSetWithExpectedSize(cs.size());
+      for (RevCommit commit : commits.values()) {
+        for (RevCommit parent : commit.getParents()) {
+          allParents.add(parent.getId());
+        }
+      }
+
+      for (ChangeData change : targetBranch) {
+        RevCommit commit = commits.get(change.getId());
+        boolean isMergeCommit = commit.getParentCount() > 1;
+        boolean isLastInChain = !allParents.contains(commit.getId());
+
+        // Recheck mergeability rather than using value stored in the index,
+        // which may be stale.
+        // TODO(dborowitz): This is ugly; consider providing a way to not read
+        // stored fields from the index in the first place.
+        change.setMergeable(null);
+        Boolean mergeable = change.isMergeable();
+        if (mergeable == null) {
+          // Skip whole check, cannot determine if mergeable
+          return null;
+        }
+        if (mergeable) {
+          mergeabilityMap.remove(change);
+        }
+
+        if (isLastInChain && isMergeCommit && mergeable) {
+          for (ChangeData c : targetBranch) {
+            mergeabilityMap.remove(c);
+          }
+          break;
+        }
+      }
+    }
+    return mergeabilityMap;
+  }
+
+  private HashMap<Change.Id, RevCommit> findCommits(
+      Collection<ChangeData> changes, Project.NameKey project)
+          throws IOException, OrmException {
+    HashMap<Change.Id, RevCommit> commits = new HashMap<>();
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk walk = new RevWalk(repo)) {
+      for (ChangeData change : changes) {
+        RevCommit commit = walk.parseCommit(ObjectId.fromString(
+            psUtil.current(dbProvider.get(), change.notes()).getRevision().get()));
+        commits.put(change.getId(), commit);
+      }
+    }
+    return commits;
+  }
+
   private RevisionResource onBehalfOf(RevisionResource rsrc, SubmitInput in)
       throws AuthException, UnprocessableEntityException, OrmException {
     ChangeControl caller = rsrc.getControl();
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..1596e31
--- /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.ReviewersUtil;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+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/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index e09b8a7..693fb92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -47,6 +47,7 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.NoSuchRefException;
+import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
@@ -612,7 +613,7 @@
   private ChangeContext newChangeContext(Change.Id id) throws Exception {
     Change c = newChanges.get(id);
     if (c == null) {
-      c = db.changes().get(id);
+      c = unwrap(db).changes().get(id);
     }
     // Pass in preloaded change to controlFor, to avoid:
     //  - reading from a db that does not belong to this update
@@ -629,4 +630,11 @@
       op.postUpdate(ctx);
     }
   }
+
+  private static ReviewDb unwrap(ReviewDb db) {
+    if (db instanceof DisabledChangesReviewDbWrapper) {
+      db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
+    }
+    return db;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
index 14be8c1..f3b2ac9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
@@ -127,7 +127,7 @@
       checkArgument(changeId.equals(ctl.getId()),
           "Approval %s does not match change %s",
           psa.getKey(), ctl.getChange().getKey());
-      if (psa.isSubmit()) {
+      if (psa.isLegacySubmit()) {
         unchanged.add(psa);
         continue;
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 586fed9..4a2eee4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -253,7 +253,7 @@
         continue;
       }
 
-      if (a.isSubmit()) {
+      if (a.isLegacySubmit()) {
         // Submit is treated specially, below (becomes committer)
         //
         if (submitAudit == null
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 5c3bcad..72517dc 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
@@ -73,6 +73,7 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
@@ -2560,6 +2561,16 @@
           msg.setMessage(msgBuf.toString());
           cmUtil.addChangeMessage(ctx.getDb(), update, msg);
 
+          PatchSetApproval submitter = new PatchSetApproval(
+                new PatchSetApproval.Key(
+                    change.currentPatchSetId(),
+                    ctx.getUser().getAccountId(),
+                    LabelId.legacySubmit()),
+                    (short) 1, ctx.getWhen());
+          update.putApproval(submitter.getLabel(), submitter.getValue());
+          ctx.getDb().patchSetApprovals().upsert(
+              Collections.singleton(submitter));
+
           return true;
         }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index 0034ce4..3de1fc1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -301,7 +301,7 @@
       for (PatchSetApproval a : approvalsUtil.byPatchSetUser(ctx.getDb(),
           ctx.getControl(), priorPatchSetId,
           ctx.getUser().getAccountId())) {
-        if (a.isSubmit()) {
+        if (a.isLegacySubmit()) {
           continue;
         }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index bea5ade..6c7a4c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -129,7 +129,7 @@
           .setValidatePolicy(CommitValidators.Policy.NONE);
       try {
         rebaseOp.updateRepo(ctx);
-      } catch (MergeConflictException e) {
+      } catch (MergeConflictException | NoSuchChangeException e) {
         toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
         throw new IntegrationException(
             "Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
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 3cbd55d..bdf9f052 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
@@ -337,7 +337,7 @@
           new PatchSetApproval.Key(
               psId,
               ctx.getUser().getAccountId(),
-              LabelId.SUBMIT),
+              LabelId.legacySubmit()),
               (short) 1, ctx.getWhen());
     byKey.put(submitter.getKey(), submitter);
     submitter.setValue((short) 1);
@@ -373,7 +373,7 @@
     // TODO(dborowitz): Don't use a label in notedb; just check when status
     // change happened.
     for (PatchSetApproval psa : normalized.unchanged()) {
-      if (includeUnchanged || psa.isSubmit()) {
+      if (includeUnchanged || psa.isLegacySubmit()) {
         logDebug("Adding submit label " + psa);
         update.putApprovalFor(
             psa.getAccountId(), psa.getLabel(), psa.getValue());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index bc9b2a1..f54e071 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -20,11 +20,8 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.PluginConfig;
@@ -97,9 +94,7 @@
         + "The change must be submitted by a Gerrit administrator.";
 
     private final AllProjectsName allProjectsName;
-    private final ReviewDb db;
     private final ProjectCache projectCache;
-    private final ApprovalsUtil approvalsUtil;
     private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
 
     public interface Factory {
@@ -108,13 +103,10 @@
 
     @Inject
     public ProjectConfigValidator(AllProjectsName allProjectsName,
-        ReviewDb db, ProjectCache projectCache,
-        ApprovalsUtil approvalsUtil,
+        ProjectCache projectCache,
         DynamicMap<ProjectConfigEntry> pluginConfigEntries) {
       this.allProjectsName = allProjectsName;
-      this.db = db;
       this.projectCache = projectCache;
-      this.approvalsUtil = approvalsUtil;
       this.pluginConfigEntries = pluginConfigEntries;
     }
 
@@ -142,11 +134,6 @@
             }
           } else {
             if (!oldParent.equals(newParent)) {
-              PatchSetApproval psa =
-                  approvalsUtil.getSubmitter(db, commit.notes(), patchSetId);
-              if (psa == null) {
-                throw new MergeValidationException(SET_BY_ADMIN);
-              }
               if (!caller.getCapabilities().canAdministrateServer()) {
                 throw new MergeValidationException(SET_BY_ADMIN);
               }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index f4ef4af..2248360 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -359,7 +359,7 @@
           Set<String> allApprovals = Sets.newHashSet();
           Set<String> distinctApprovals = Sets.newHashSet();
           for (PatchSetApproval a : input.currentApprovals()) {
-            if (a.getValue() != 0 && !a.isSubmit()) {
+            if (a.getValue() != 0 && !a.isLegacySubmit()) {
               allApprovals.add(formatLabel(a.getLabel(), a.getValue(),
                   a.getAccountId()));
               distinctApprovals.add(formatLabel(a.getLabel(), a.getValue()));
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 c96ed65..a0860c7 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
@@ -184,6 +184,15 @@
         : Futures.<Object, IOException> immediateCheckedFuture(null);
   }
 
+  /**
+   * Synchronously delete a change.
+   *
+   * @param id change ID to delete.
+   */
+  public void delete(Change.Id id) throws IOException {
+    new DeleteTask(id).call();
+  }
+
   private Collection<ChangeIndex> getWriteIndexes() {
     return indexes != null
         ? indexes.getWriteIndexes()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index b2e765c..dba863e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -61,6 +61,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -186,7 +187,7 @@
 
     public ChangeNotes create(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException {
-      Change change = db.changes().get(changeId);
+      Change change = unwrap(db).changes().get(changeId);
       checkArgument(change.getProject().equals(project),
           "passed project %s when creating ChangeNotes for %s, but actual"
           + " project is %s",
@@ -220,7 +221,7 @@
         ReviewDb db, Change.Id changeId) throws OrmException {
     checkState(!migration.readChanges(), "do not call"
         + " createFromIdOnlyWhenNotedbDisabled when notedb is enabled");
-      Change change = db.changes().get(changeId);
+      Change change = unwrap(db).changes().get(changeId);
       return new ChangeNotes(repoManager, migration, allUsers,
           change.getProject(), change).load();
     }
@@ -242,7 +243,7 @@
         final ListeningExecutorService executorService, final ReviewDb db,
         final Project.NameKey project, final Change.Id changeId) {
       return Futures.makeChecked(
-          Futures.transformAsync(db.changes().getAsync(changeId),
+          Futures.transformAsync(unwrap(db).changes().getAsync(changeId),
               new AsyncFunction<Change, ChangeNotes>() {
                 @Override
                 public ListenableFuture<ChangeNotes> apply(
@@ -284,7 +285,7 @@
         return notes;
       }
 
-      for (Change c : db.changes().get(changeIds)) {
+      for (Change c : unwrap(db).changes().get(changeIds)) {
         notes.add(createFromChangeOnlyWhenNotedbDisabled(c));
       }
       return notes;
@@ -304,7 +305,7 @@
         return notes;
       }
 
-      for (Change c : db.changes().get(changeIds)) {
+      for (Change c : unwrap(db).changes().get(changeIds)) {
         if (c != null && project.equals(c.getDest().getParentKey())) {
           ChangeNotes cn = createFromChangeOnlyWhenNotedbDisabled(c);
           if (predicate.apply(cn)) {
@@ -330,7 +331,7 @@
           }
         }
       } else {
-        for (Change change : db.changes().all()) {
+        for (Change change : unwrap(db).changes().all()) {
           ChangeNotes notes = createFromChangeOnlyWhenNotedbDisabled(change);
           if (predicate.apply(notes)) {
             m.put(change.getProject(), notes);
@@ -356,7 +357,7 @@
       // A batch size of N may overload get(Iterable), so use something smaller,
       // but still >1.
       for (List<Change.Id> batch : Iterables.partition(ids, 30)) {
-        for (Change change : db.changes().get(batch)) {
+        for (Change change : unwrap(db).changes().get(batch)) {
           notes.add(createFromChangeOnlyWhenNotedbDisabled(change));
         }
       }
@@ -385,6 +386,13 @@
       }
       return ids;
     }
+
+    private static ReviewDb unwrap(ReviewDb db) {
+      if (db instanceof DisabledChangesReviewDbWrapper) {
+        db = ((DisabledChangesReviewDbWrapper) db).unsafeGetDelegate();
+      }
+      return db;
+    }
   }
 
   private final Project.NameKey project;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index e479ba9..2704be8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -150,9 +150,13 @@
     if (tw == null) {
       return Text.EMPTY;
     }
-    if (tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) {
+    if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) {
+      return new Text(repo.open(tw.getObjectId(0), Constants.OBJ_BLOB));
+    } else if (tw.getFileMode(0).getObjectType() == Constants.OBJ_COMMIT) {
+      String str = "Subproject commit " + ObjectId.toString(tw.getObjectId(0));
+      return new Text(str.getBytes());
+    } else {
       return Text.EMPTY;
     }
-    return new Text(repo.open(tw.getObjectId(0), Constants.OBJ_BLOB));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index 2caa937..2b5e235 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -32,7 +32,6 @@
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.patch.CombinedFileHeader;
 import org.eclipse.jgit.patch.FileHeader;
 import org.eclipse.jgit.util.IntList;
@@ -97,10 +96,7 @@
 
     header = compact(hdr);
 
-    if (hdr instanceof CombinedFileHeader
-        || hdr.getHunks().isEmpty() //
-        || hdr.getOldMode() == FileMode.GITLINK
-        || hdr.getNewMode() == FileMode.GITLINK) {
+    if (hdr instanceof CombinedFileHeader || hdr.getHunks().isEmpty()) {
       edits = Collections.emptyList();
     } else {
       edits = Collections.unmodifiableList(editList);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 10ea61c..63693e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -312,14 +312,6 @@
 
   private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader,
       long size, long sizeDelta) {
-    final FileMode oldMode = fileHeader.getOldMode();
-    final FileMode newMode = fileHeader.getNewMode();
-
-    if (oldMode == FileMode.GITLINK || newMode == FileMode.GITLINK) {
-      return new PatchListEntry(fileHeader, Collections.<Edit> emptyList(),
-          size, sizeDelta);
-    }
-
     if (aTree == null // want combined diff
         || fileHeader.getPatchType() != PatchType.UNIFIED
         || fileHeader.getHunks().isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 9072c2a..51c70f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -179,9 +179,7 @@
     }
 
     boolean hugeFile = false;
-    if (a.mode == FileMode.GITLINK || b.mode == FileMode.GITLINK) {
-      // Do nothing
-    } else if (a.src == b.src && a.size() <= context
+    if (a.src == b.src && a.size() <= context
         && content.getEdits().isEmpty()) {
       // Odd special case; the files are identical (100% rename or copy)
       // and the user has asked for context that is larger than the file.
@@ -471,6 +469,10 @@
           } else if (mode.getObjectType() == Constants.OBJ_BLOB) {
             srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB));
 
+          } else if (mode.getObjectType() == Constants.OBJ_COMMIT) {
+            String strContent = "Subproject commit " + ObjectId.toString(id);
+            srcContent = strContent.getBytes();
+
           } else {
             srcContent = Text.NO_BYTES;
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 445c8c5..fec858a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
 import com.google.gerrit.server.config.RepositoryConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -102,6 +103,7 @@
   private final PersonIdent serverIdent;
   private final Provider<CurrentUser> currentUser;
   private final Provider<PutConfig> putConfig;
+  private final AllProjectsName allProjects;
   private final String name;
 
   @Inject
@@ -120,6 +122,7 @@
       @GerritPersonIdent PersonIdent serverIdent,
       Provider<CurrentUser> currentUser,
       Provider<PutConfig> putConfig,
+      AllProjectsName allProjects,
       @Assisted String name) {
     this.projectsCollection = projectsCollection;
     this.groupsCollection = groupsCollection;
@@ -137,6 +140,7 @@
     this.serverIdent = serverIdent;
     this.currentUser = currentUser;
     this.putConfig = putConfig;
+    this.allProjects = allProjects;
     this.name = name;
   }
 
@@ -155,9 +159,9 @@
     CreateProjectArgs args = new CreateProjectArgs();
     args.setProjectName(ProjectUtil.stripGitSuffix(name));
 
-    if (!Strings.isNullOrEmpty(input.parent)) {
-      args.newParent = projectsCollection.get().parse(input.parent).getControl();
-    }
+    String parentName = MoreObjects.firstNonNull(
+        Strings.emptyToNull(input.parent), allProjects.get());
+    args.newParent = projectsCollection.get().parse(parentName).getControl();
     args.createEmptyCommit = input.createEmptyCommit;
     args.permissionsOnly = input.permissionsOnly;
     args.projectDescription = Strings.emptyToNull(input.description);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index 8113c01..01aacfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
@@ -70,18 +72,18 @@
       throws AuthException, ResourceConflictException,
       ResourceNotFoundException, UnprocessableEntityException, IOException {
     ProjectControl ctl = rsrc.getControl();
-    validateParentUpdate(ctl, input.parent, checkIfAdmin);
+    String parentName = MoreObjects.firstNonNull(
+        Strings.emptyToNull(input.parent), allProjects.get());
+    validateParentUpdate(ctl, parentName, checkIfAdmin);
     try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
       ProjectConfig config = ProjectConfig.read(md);
       Project project = config.getProject();
-      project.setParentName(Strings.emptyToNull(input.parent));
+      project.setParentName(parentName);
 
       String msg = Strings.emptyToNull(input.commitMessage);
       if (msg == null) {
         msg = String.format(
-            "Changed parent to %s.\n",
-            MoreObjects.firstNonNull(project.getParentName(),
-                allProjects.get()));
+              "Changed parent to %s.\n", parentName);
       } else if (!msg.endsWith("\n")) {
         msg += "\n";
       }
@@ -90,8 +92,9 @@
       config.commit(md);
       cache.evict(ctl.getProject());
 
-      Project.NameKey parentName = project.getParent(allProjects);
-      return parentName != null ? parentName.get() : "";
+      Project.NameKey parent = project.getParent(allProjects);
+      checkNotNull(parent);
+      return parent.get();
     } catch (RepositoryNotFoundException notFound) {
       throw new ResourceNotFoundException(rsrc.getName());
     } catch (ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index aa12b91..b2930e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -857,7 +857,7 @@
   public Optional<PatchSetApproval> getSubmitApproval()
     throws OrmException {
     for (PatchSetApproval psa : currentApprovals()) {
-      if (psa.isSubmit()) {
+      if (psa.isLegacySubmit()) {
         return Optional.fromNullable(psa);
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DisabledChangesReviewDbWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DisabledChangesReviewDbWrapper.java
index 005b3b1..3501374 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DisabledChangesReviewDbWrapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DisabledChangesReviewDbWrapper.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
 import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
 import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
 import com.google.gerrit.reviewdb.server.PatchSetAccess;
@@ -33,6 +34,7 @@
 public class DisabledChangesReviewDbWrapper extends ReviewDbWrapper {
   private static final String MSG = "This table has been migrated to notedb";
 
+  private final DisabledChangeAccess changes;
   private final DisabledPatchSetApprovalAccess patchSetApprovals;
   private final DisabledChangeMessageAccess changeMessages;
   private final DisabledPatchSetAccess patchSets;
@@ -40,6 +42,7 @@
 
   DisabledChangesReviewDbWrapper(ReviewDb db) {
     super(db);
+    changes = new DisabledChangeAccess(delegate.changes());
     patchSetApprovals =
         new DisabledPatchSetApprovalAccess(delegate.patchSetApprovals());
     changeMessages = new DisabledChangeMessageAccess(delegate.changeMessages());
@@ -53,6 +56,11 @@
   }
 
   @Override
+  public ChangeAccess changes() {
+    return changes;
+  }
+
+  @Override
   public PatchSetApprovalAccess patchSetApprovals() {
     return patchSetApprovals;
   }
@@ -72,6 +80,38 @@
     return patchComments;
   }
 
+  private static class DisabledChangeAccess extends ChangeAccessWrapper {
+
+    protected DisabledChangeAccess(ChangeAccess delegate) {
+      super(delegate);
+    }
+
+    @Override
+    public ResultSet<Change> iterateAllEntities() {
+      throw new UnsupportedOperationException(MSG);
+    }
+
+    @Override
+    public CheckedFuture<Change, OrmException> getAsync(Change.Id key) {
+      throw new UnsupportedOperationException(MSG);
+    }
+
+    @Override
+    public ResultSet<Change> get(Iterable<Change.Id> keys) {
+      throw new UnsupportedOperationException(MSG);
+    }
+
+    @Override
+    public Change get(Change.Id id) throws OrmException {
+      throw new UnsupportedOperationException(MSG);
+    }
+
+    @Override
+    public ResultSet<Change> all() throws OrmException {
+      throw new UnsupportedOperationException(MSG);
+    }
+  }
+
   private static class DisabledPatchSetApprovalAccess
       extends PatchSetApprovalAccessWrapper {
     DisabledPatchSetApprovalAccess(PatchSetApprovalAccess delegate) {
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/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/plugins/reviewnotes b/plugins/reviewnotes
index 47c74e2..0ea78c9 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 47c74e2da9443e028d8d52d95791e0cd15add822
+Subproject commit 0ea78c9ca7c70515c81681af05a65ec4dc32a542
diff --git a/polygerrit-ui/app/BUCK b/polygerrit-ui/app/BUCK
index 93cf614..5110350 100644
--- a/polygerrit-ui/app/BUCK
+++ b/polygerrit-ui/app/BUCK
@@ -6,6 +6,7 @@
   ['**'],
   excludes = [
     'BUCK',
+    '**/*_test.html',
     'index.html',
   ] + WCT_TEST_PATTERNS + PY_TEST_PATTERNS)
 
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-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index 5b03274..6788cce 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -66,7 +66,7 @@
     <gr-ajax
         auto
         url="/changes/"
-        params="[[_computeQueryParams(_query, _offset)]]"
+        params="[[_computeQueryParams(_query, _offset, changesPerPage)]]"
         last-response="{{_changes}}"
         last-error="{{_lastError}}"
         loading="{{_loading}}"></gr-ajax>
@@ -80,10 +80,11 @@
           selected-index="{{viewState.selectedChangeIndex}}"
           show-star="[[loggedIn]]"></gr-change-list>
       <nav>
-        <a href$="[[_computeNavLink(_query, _offset, -1)]]"
+        <a href$="[[_computeNavLink(_query, _offset, -1, changesPerPage)]]"
            hidden$="[[_hidePrevArrow(_offset)]]">&larr; Prev</a>
-        <a href$="[[_computeNavLink(_query, _offset, 1)]]"
-           hidden$="[[_hideNextArrow(_changes.length)]]">Next &rarr;</a>
+        <a href$="[[_computeNavLink(_query, _offset, 1, changesPerPage)]]"
+           hidden$="[[_hideNextArrow(_changes.length, changesPerPage)]]">
+          Next &rarr;</a>
       </nav>
     </div>
   </template>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index d0a97c1d..a694fc9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -14,8 +14,6 @@
 (function() {
   'use strict';
 
-  var DEFAULT_NUM_CHANGES = 25;
-
   Polymer({
     is: 'gr-change-list-view',
 
@@ -51,6 +49,8 @@
         value: function() { return {}; },
       },
 
+      changesPerPage: Number,
+
       /**
        * Currently active query.
        */
@@ -103,13 +103,13 @@
       this.fire('title-change', {title: this._query});
     },
 
-    _computeQueryParams: function(query, offset) {
+    _computeQueryParams: function(query, offset, changesPerPage) {
       var options = this.listChangesOptionsToHex(
           this.ListChangesOption.LABELS,
           this.ListChangesOption.DETAILED_ACCOUNTS
       );
       var obj = {
-        n: DEFAULT_NUM_CHANGES,  // Number of results to return.
+        n: changesPerPage,
         O: options,
         S: offset || 0,
       };
@@ -119,10 +119,10 @@
       return obj;
     },
 
-    _computeNavLink: function(query, offset, direction) {
+    _computeNavLink: function(query, offset, direction, changesPerPage) {
       // Offset could be a string when passed from the router.
       offset = +(offset || 0);
-      var newOffset = Math.max(0, offset + (25 * direction));
+      var newOffset = Math.max(0, offset + (changesPerPage * direction));
       var href = '/q/' + query;
       if (newOffset > 0) {
         href += ',' + newOffset;
@@ -142,8 +142,8 @@
       return offset == 0;
     },
 
-    _hideNextArrow: function(changesLen) {
-      return changesLen < DEFAULT_NUM_CHANGES;
+    _hideNextArrow: function(changesLen, changesPerPage) {
+      return changesLen < changesPerPage;
     },
   });
 })();
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-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 41cb058..1dbdbfb 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
@@ -121,6 +121,7 @@
       .commitMessage {
         font-family: var(--monospace-font-family);
         flex: 0 0 72ch;
+        overflow: auto;
         margin-right: 2em;
         margin-bottom: 1em;
       }
@@ -129,6 +130,9 @@
         font-weight: bold;
         margin-bottom: .25em;
       }
+      .commitMessage gr-linked-text {
+        --linked-text-white-space: pre;
+      }
       .commitAndRelated {
         align-content: flex-start;
         display: flex;
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..f5028a9 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">
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..bc7b2a6 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">
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_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..4538cec 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: 1.3em;
+      width: 1.3em;
+      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="32"></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/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 129a321..af4a0e4 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -25,9 +25,11 @@
   <template>
     <style>
       :host {
+        display: block;
+      }
+      nav {
         align-items: center;
         display: flex;
-        overflow: hidden;
       }
       .bigTitle {
         color: var(--primary-text-color);
@@ -37,6 +39,60 @@
       .bigTitle:hover {
         text-decoration: underline;
       }
+      ul {
+        list-style: none;
+      }
+      .links {
+        margin-left: 1em;
+      }
+      .links ul {
+        display: none;
+      }
+      .links > li {
+        cursor: default;
+        display: inline-block;
+        margin-left: 1em;
+        padding: .4em 0;
+        position: relative;
+      }
+      .links li:hover ul {
+        background-color: #fff;
+        box-shadow: 0 1px 1px rgba(0, 0, 0, .3);
+        display: block;
+        left: -.75em;
+        position: absolute;
+        top: 2em;
+        z-index: 1000;
+      }
+      .links li ul li a:link,
+      .links li ul li a:visited {
+        color: #00e;
+        display: block;
+        padding: .5em .75em;
+        text-decoration: none;
+        white-space: nowrap;
+      }
+      .links li ul li:hover a {
+        background-color: var(--selection-background-color);
+      }
+      .linksTitle {
+        display: inline-block;
+        padding-right: 1em;
+        position: relative;
+      }
+      .downArrow {
+        border-left: .36em solid transparent;
+        border-right: .36em solid transparent;
+        border-top: .36em solid #ccc;
+        height: 0;
+        position: absolute;
+        right: 0;
+        top: calc(50% - .1em);
+        width: 0;
+      }
+      .links li:hover .downArrow {
+        border-top-color: #666;
+      }
       .rightItems {
         display: flex;
         flex: 1;
@@ -73,14 +129,30 @@
         }
       }
     </style>
-    <a href="/" class="bigTitle">PolyGerrit</a>
-    <div class="rightItems">
-      <gr-search-bar value="{{params.query}}" role="search"></gr-search-bar>
-      <div class="accountContainer" id="accountContainer">
-        <a class="loginButton" href="/login" on-tap="_loginTapHandler">Login</a>
-        <gr-account-dropdown account="[[account]]"></gr-account-dropdown>
+    <nav>
+      <a href="/" class="bigTitle">PolyGerrit</a>
+      <ul class="links">
+        <template is="dom-repeat" items="[[_links]]" as="linkGroup">
+          <li>
+            <span class="linksTitle">
+              [[linkGroup.title]] <i class="downArrow"></i>
+            </span>
+            <ul>
+              <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
+                <li><a href="[[link.url]]">[[link.name]]</a></li>
+              </template>
+            </ul>
+          </li>
+        </template>
+      </ul>
+      <div class="rightItems">
+        <gr-search-bar value="{{searchQuery}}" role="search"></gr-search-bar>
+        <div class="accountContainer" id="accountContainer">
+          <a class="loginButton" href="/login" on-tap="_loginTapHandler">Login</a>
+          <gr-account-dropdown account="[[_account]]"></gr-account-dropdown>
+        </div>
       </div>
-    </div>
+    </nav>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-main-header.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index ca57625..173b88e 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -14,6 +14,24 @@
 (function() {
   'use strict';
 
+  var DEFAULT_LINKS = [{
+    title: 'Changes',
+    links: [
+      {
+        url: '/q/status:open',
+        name: 'Open',
+      },
+      {
+        url: '/q/status:merged',
+        name: 'Merged',
+      },
+      {
+        url: '/q/status:abandoned',
+        name: 'Abandoned',
+      },
+    ],
+  }];
+
   Polymer({
     is: 'gr-main-header',
 
@@ -22,14 +40,74 @@
     },
 
     properties: {
+      searchQuery: {
+        type: String,
+        notify: true,
+      },
+
+      _account: Object,
+      _defaultLinks: {
+        type: Array,
+        value: function() {
+          return DEFAULT_LINKS;
+        },
+      },
+      _links: {
+        type: Array,
+        computed: '_computeLinks(_defaultLinks, _userLinks)',
+      },
+      _userLinks: {
+        type: Array,
+        value: function() { return []; },
+      },
     },
 
+    observers: [
+      '_accountLoaded(_account)',
+    ],
+
     attached: function() {
+      this._loadAccount();
+    },
+
+    _computeLinks: function(defaultLinks, userLinks) {
+      var links = defaultLinks.slice();
+      if (userLinks && userLinks.length > 0) {
+        links.push({
+          title: 'Your',
+          links: userLinks,
+        });
+      }
+      return links;
+    },
+
+    _loadAccount: function() {
       this.$.restAPI.getAccount().then(function(account) {
-        var loggedIn = !!account;
-        this.$.accountContainer.classList.toggle('loggedIn', loggedIn);
-        this.$.accountContainer.classList.toggle('loggedOut', !loggedIn);
+        this._account = account;
+        this.$.accountContainer.classList.toggle('loggedIn', account != null);
+        this.$.accountContainer.classList.toggle('loggedOut', account == null);
       }.bind(this));
     },
+
+    _accountLoaded: function(account) {
+      if (!account) { return; }
+
+      this.$.restAPI.getPreferences().then(function(prefs) {
+        this._userLinks =
+            prefs.my.map(this._stripHashPrefix).filter(this._isSupportedLink);
+      }.bind(this));
+    },
+
+    _stripHashPrefix: function(linkObj) {
+      if (linkObj.url.indexOf('#') === 0) {
+        linkObj.url = linkObj.url.slice(1);
+      }
+      return linkObj;
+    },
+
+    _isSupportedLink: function(linkObj) {
+      // Groups are not yet supported.
+      return linkObj.url.indexOf('/groups') !== 0;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
new file mode 100644
index 0000000..0b40d87
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-main-header</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-main-header.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-main-header></gr-main-header>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-main-header tests', function() {
+    var element;
+
+    setup(function() {
+      stub('gr-main-header', {
+        _loadAccount: function() {},
+      });
+      element = fixture('basic');
+    });
+
+    test('strip hash prefix', function() {
+      assert.deepEqual([
+        {url: '#/q/owner:self+is:draft'},
+        {url: 'https://awesometown.com/#hashyhash'},
+      ].map(element._stripHashPrefix),
+      [
+        {url: '/q/owner:self+is:draft'},
+        {url: 'https://awesometown.com/#hashyhash'},
+      ]);
+    });
+
+    test('filter unsupported urls', function() {
+      assert.deepEqual([
+        {url: '/q/owner:self+is:draft'},
+        {url: '/c/331788/'},
+        {url: '/groups/self'},
+        {url: 'https://awesometown.com/#hashyhash'},
+      ].filter(element._isSupportedLink),
+      [
+        {url: '/q/owner:self+is:draft'},
+        {url: '/c/331788/'},
+        {url: 'https://awesometown.com/#hashyhash'},
+      ]);
+    });
+
+    test('user links', function() {
+      var defaultLinks = [{
+        title: 'Faves',
+        links: [{
+          name: 'Pinterest',
+          url: 'https://pinterest.com',
+        }],
+      }];
+      var userLinks = [{
+        name: 'Facebook',
+        url: 'https://facebook.com',
+      }];
+      assert.deepEqual(element._computeLinks(defaultLinks, []), defaultLinks);
+      assert.deepEqual(element._computeLinks(defaultLinks, userLinks),
+          defaultLinks.concat({
+            title: 'Your',
+            links: userLinks,
+          }));
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index c8c4523..f559794 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -30,12 +30,11 @@
       }
       input {
         border: 1px solid #d1d2d3;
-        outline: none;
-      }
-      input {
+        border-radius: 2px 0 0 2px;
         flex: 1;
         font: inherit;
-        border-radius: 2px 0 0 2px;
+        outline: none;
+        padding: 0 .25em;
       }
       gr-button {
         background-color: #f1f2f3;
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_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 921d766..f3c1402 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">
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index fd6525d..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,29 +55,25 @@
         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></gr-main-header>
+    <gr-main-header search-query="{{params.query}}"></gr-main-header>
     <main>
       <template is="dom-if" if="{{_showChangeListView}}" restamp="true">
         <gr-change-list-view
             params="[[params]]"
             view-state="{{_viewState.changeListView}}"
-            logged-in="[[_computeLoggedIn(account)]]"></gr-change-list-view>
+            changes-per-page="[[_preferences.changes_per_page]]"
+            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">
@@ -90,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 475ea0d..31797dd 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,28 +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,
@@ -74,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));
     },
 
@@ -104,8 +99,14 @@
 
     _accountChanged: function() {
       this._resolveAccountReady();
+
       if (this.loggedIn) {
-        this.$.diffPreferencesXHR.generateRequest();
+        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
@@ -125,11 +126,11 @@
           tab_size: 8,
           theme: 'DEFAULT',
         };
-      }
-    },
 
-    _configChanged: function(config) {
-      this._resolveConfigReady(config);
+        this._preferences = {
+          changes_per_page: 25,
+        };
+      }
     },
 
     _viewChanged: function(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-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
index 68a98e8..37bca2e 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
@@ -24,8 +24,8 @@
         display: block;
       }
       :host([pre]) span {
-        white-space: pre-wrap;
-        word-wrap: break-word;
+        white-space: var(--linked-text-white-space, pre-wrap);
+        word-wrap: var(--linked-text-work-wrap, break-word);
       }
       :host([disabled]) a {
         color: inherit;
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 f69f8b1..27cd989 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,17 +82,33 @@
       }.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');
     },
 
+    getPreferences: function() {
+      return this._fetchSharedCacheURL('/accounts/self/preferences');
+    },
+
     _fetchSharedCacheURL: function(url) {
       if (this._sharedFetchPromises[url]) {
         return this._sharedFetchPromises[url];
       }
       // TODO(andybons): Periodic cache invalidation.
       if (this._cache[url] !== undefined) {
-        return this._cache[url];
+        return Promise.resolve(this._cache[url]);
       }
       this._sharedFetchPromises[url] = this.fetchJSON(url).then(
         function(response) {
@@ -104,7 +120,7 @@
         }.bind(this)).catch(function(err) {
           this._sharedFetchPromises[url] = undefined;
           throw err;
-        });
+        }.bind(this));
       return this._sharedFetchPromises[url];
     },
 
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 c397c8f..3902707f 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,14 +65,26 @@
 
       Promise.all(promises).then(function(results) {
         assert.deepEqual(results, [1, 1, 1]);
-        fetchJSONStub.restore();
+        element._fetchSharedCacheURL('/foo').then(function(foo) {
+          assert.equal(foo, 1);
+          fetchJSONStub.restore();
+          done();
+        });
+      });
+    });
+
+    test('cached promise', function(done) {
+      var promise = Promise.reject('foo');
+      element._cache['/foo'] = promise;
+      element._fetchSharedCacheURL('/foo').catch(function(p) {
+        assert.equal(p, 'foo');
         done();
       });
     });
 
     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{}');
         }});
       });
@@ -89,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; }
         }});
       });
@@ -128,6 +140,7 @@
           assert.deepEqual(obj.comments[0], {
             message: 'this isn’t quite right',
           });
+          fetchJSONStub.restore();
           done();
         });
     });
@@ -178,6 +191,7 @@
           assert.deepEqual(obj.comments[1], {
             message: '¯\\_(ツ)_/¯',
           });
+          fetchJSONStub.restore();
           done();
         });
     });
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index df48e8a..11ea3b4 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -19,6 +19,7 @@
   --primary-text-color: #000;
   --search-border-color: #ddd;
   --secondary-color: #f1f2f3;
+  --selection-background-color: #ebf5fb;
   --default-text-color: #000;
   --view-background-color: #fff;
   --default-horizontal-margin: 1.25rem;
diff --git a/polygerrit-ui/app/scripts/fake-app.js b/polygerrit-ui/app/test/fake-app.js
similarity index 100%
rename from polygerrit-ui/app/scripts/fake-app.js
rename to polygerrit-ui/app/test/fake-app.js