Merge "AccountApi: Add methods to get and set GeneralPreferencesInfo"
diff --git a/Documentation/cmd-index-activate.txt b/Documentation/cmd-index-activate.txt
index 37783ef..0135cfe 100644
--- a/Documentation/cmd-index-activate.txt
+++ b/Documentation/cmd-index-activate.txt
@@ -5,7 +5,7 @@
 
 == SYNOPSIS
 --
-'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index activate <index>'
+'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index activate'
 --
 
 == DESCRIPTION
@@ -18,9 +18,6 @@
 This command allows to activate the latest index even if there were some
 failures.
 
-The <index> argument controls which secondary index is activated. Currently, the
-only supported value is "changes".
-
 == ACCESS
 Caller must be a member of the privileged 'Administrators' group.
 
diff --git a/Documentation/cmd-index-start.txt b/Documentation/cmd-index-start.txt
index cee283e..4148b24 100644
--- a/Documentation/cmd-index-start.txt
+++ b/Documentation/cmd-index-start.txt
@@ -5,7 +5,7 @@
 
 == SYNOPSIS
 --
-'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index start <index>'
+'ssh' -p @SSH_PORT@ @SSH_HOST@ 'gerrit index start'
 --
 
 == DESCRIPTION
@@ -19,9 +19,6 @@
 Gerrit. This command will not start the indexer if it is already running or if
 the active index is the latest.
 
-The <index> argument controls which secondary index is started. Currently, the
-only supported value is "changes".
-
 == ACCESS
 Caller must be a member of the privileged 'Administrators' group.
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index a10083d..6ca9bdc 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3990,7 +3990,7 @@
 [[submodule]]
 === Section submodule
 
-[[submodule.verbosesuperprojectupdate]]submodule.verboseSuperprojectUpdate
+[[submodule.verbosesuperprojectupdate]]submodule.verboseSuperprojectUpdate::
 +
 When using link:user-submodules.html#automatic_update[automatic superproject updates]
 this option will determine if the submodule commit messages are included into
@@ -3998,6 +3998,11 @@
 +
 By default this is true.
 
+[[submodule.enableSuperProjectSubscriptions]]submodule.enableSuperProjectSubscriptions::
++
+This allows to enable the superproject subscription mechanism.
++
+By default this is true.
 
 [[user]]
 === Section user
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index da213a8..a97cdf2 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -56,6 +56,12 @@
 text that will be appended to emails related to a user submitting comments on
 changes.  See `ChangeSubject.vm`, `Comment.vm` and `ChangeFooter.vm`.
 
+=== DeleteVote.vm
+
+The `DeleteVote.vm` template will determine the contents of the email related
+to removing votes on changes.  It is a `ChangeEmail`: see `ChangeSubject.vm`
+and `ChangeFooter.vm`.
+
 === Footer.vm
 
 The `Footer.vm` template will determine the contents of the footer text
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index abb332e..501f986 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1065,6 +1065,9 @@
 +
 The link:rest-api-changes.html#change-info[ChangeInfo] entity for the
 current change.
++
+The link:rest-api-changes.html#revision-info[RevisionInfo] entity for
+the current patch set.
 
 * Project Info Screen:
 ** `GerritUiExtensionPoint.PROJECT_INFO_SCREEN_TOP`:
diff --git a/Documentation/doc.css.in b/Documentation/doc.css.in
index 429e81c..e09e426 100644
--- a/Documentation/doc.css.in
+++ b/Documentation/doc.css.in
@@ -32,7 +32,6 @@
 .listingblock > .content {
   border: 2px solid silver;
   background: #ebebeb;
-  margin-left: 2em;
   color: darkgreen;
   padding: 2px;
   overflow: auto;
@@ -58,3 +57,24 @@
 td.tableblock {
   border: 1px solid #EEE;
 }
+
+div.title {
+  color: #527bbd;
+  font-family: Arial,Helvetica,sans-serif;
+  font-weight: bold;
+  text-align: left;
+}
+
+.listingblock div.title {
+  margin-top: 1.0em;
+  margin-bottom: 0.5em;
+}
+
+div.admonitionblock {
+  margin-top: 1em;
+}
+
+div.admonitionblock td.content {
+  padding-left: 0.5em;
+  border-left: 3px solid #dddddd;
+}
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index bb9e7c5..9b283b4 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -677,6 +677,14 @@
 and `Edit Config` buttons on the project screen, and the `Follow-Up`
 button on the change screen).
 
+- [[use-flash]]`Use Flash Clipboard Widget`:
++
+Whether the Flash clipboard widget should be used. If enabled and the Flash
+plugin is available, Gerrit offers a copy-to-clipboard icon next to IDs and
+commands that need to be copied frequently, such as the Change-Ids, commit IDs
+and download commands. Note that this option is only shown if the Flash plugin
+is available and the JavaScript Clipboard API is unavailable.
+
 [[my-menu]]
 In addition it is possible to customize the menu entries of the `My`
 menu. This can be used to make the navigation to frequently used
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index d31c401..22c2b21 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -741,7 +741,7 @@
 Which of these two behaviors is desired will always depend on how a particular
 Gerrit server is managed.
 
-==== Example 9: Remove the `Verified` category
+=== Example 9: Remove the `Verified` category
 A project has no build and test. It consists of only text files and needs only
 code review.  We want to remove the `Verified` category from this project so
 that `Code-Review+2` is the only criteria for a change to become submittable.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 5603d36..51ba60f 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -983,6 +983,84 @@
   The change could not be rebased due to a path conflict during merge.
 ----
 
+[[move-change]]
+=== Move Change
+--
+'POST /changes/link:#change-id[\{change-id\}]/move'
+--
+
+Move a change.
+
+The destination branch must be provided in the request body inside a
+link:#move-input[MoveInput] entity.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/move HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "destination_branch" : "release-branch"
+  }
+
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the moved change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "myProject~release-branch~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "release-branch",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "mergeable": true,
+    "insertions": 2,
+    "deletions": 13,
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
+If the change cannot be moved because the change state doesn't
+allow moving the change, the response is "`409 Conflict`" and
+the error message is contained in the response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain; charset=UTF-8
+
+  change is merged
+----
+
+If the change cannot be moved because the user doesn't have
+abandon permission on the change or upload permission on the destination,
+the response is "`409 Conflict`" and the error message is contained in the
+response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain; charset=UTF-8
+
+  move not permitted
+----
+
 [[revert-change]]
 === Revert Change
 --
@@ -996,7 +1074,7 @@
 
 .Request
 ----
-  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revert HTTP/1.0
+  POST /changes/myProject~master~I1ffe09a505e25f15ce1521bcfb222e51e62c2a14/revert HTTP/1.0
 ----
 
 As response a link:#change-info[ChangeInfo] entity is returned that
@@ -4495,6 +4573,18 @@
 A list of other branch names where this change could merge cleanly
 |============================
 
+[[move-input]]
+=== MoveInput
+The `MoveInput` entity contains information for moving a change to a new branch.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name   ||Description
+|`destination`||Destination branch
+|`message`    |optional|
+A message to be posted in this change's comments
+|===========================
+
 [[problem-info]]
 === ProblemInfo
 The `ProblemInfo` entity contains a description of a potential consistency problem
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 672f025..2701eb9 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2016,12 +2016,10 @@
 valid link:user-changeid.html[Change-Id] footer in any commit uploaded
 for review is required. This does not apply to commits pushed directly
 to a branch or tag.
-|`enable_signed_push`|
-optional, not set if signed push is disabled|
+|`enable_signed_push`|optional, not set if signed push is disabled|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 signed push validation is enabled on the project.
-|`require_signed_push`|
-optional, not set if signed push is disabled
+|`require_signed_push`|optional, not set if signed push is disabled|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 signed push validation is required on the project.
 |`max_object_size_limit`     ||
@@ -2136,7 +2134,7 @@
 `configured_value` and `inherited_value`.
 |`values`          |optional|
 The list of values. Only set if the `type` is `ARRAY`.
-`editable`         |`false` if not set|
+|`editable`         |`false` if not set|
 Whether the value is editable.
 |`permitted_values`|optional|
 The list of permitted values. Only set if the `type` is `LIST`.
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index 151ac71..a8a0262 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -1,78 +1,118 @@
 = Gerrit Code Review - Superproject subscription to submodules updates
 
+[[automatic_update]]
 == Description
-
 Gerrit supports a custom git superproject feature for tracking submodules.
 This feature is useful for automatic updates on superprojects whenever
-a change is merged on tracked submodules. To take advantage of this
-feature, one should add submodule(s) to a local working copy of a
-superproject, edit the created .gitmodules configuration file to
-have a branch field on each submodule section with the value of the
-submodule branch it is subscribing to, commit the changes, push and
-merge the commit.
+a change is merged on tracked submodules.
 
-When a commit is merged to a project, the commit content is scanned
-to identify if it registers git submodules (if the commit registers
-any gitlinks and .gitmodules file with required info) and if so,
-a new submodule subscription is registered.
+When a superproject is subscribed to a submodule, it is not
+required to push/merge commits to this superproject to update the
+gitlink to the submodule. Whenever a commit is merged in a submodule,
+its subscribed superproject is updated by Gerrit.
 
-When a new commit of a registered submodule is merged, Gerrit
-automatically updates the subscribers to the submodule with a new
-commit having the updated gitlinks.
+Imagine a superproject called 'super' having a branch called 'dev'
+having subscribed to a submodule 'sub' on a branch 'dev-of-sub'. When a commit
+is merged in branch 'dev-of-sub' of 'sub' project, Gerrit automatically
+creates a new commit on branch 'dev' of 'super' updating the gitlink
+to point to the just merged commit.
 
-== Git Submodules Overview
+To take advantage of this feature, one should:
 
-Submodules are a git feature that allows an external repository to be
+. ensure superproject subscriptions are enabled on the server via
+  link:config-gerrit.html#submodule.enableSuperProjectSubscriptions[submodule.enableSuperProjectSubscriptions]
+. configure the submodule to allow having a superproject subscribed
+. ensure the .gitmodules file of the superproject includes
+.. a branch field
+.. a url that starts with the link:config-gerrit.html#gerrit.canonicalWebUrl[`gerrit.canonicalWebUrl`]
+
+When a commit in a project is merged, Gerrit checks for superprojects
+that are subscribed to the the project and automatically updates those
+superprojects with a commit that updates the gilink for the project.
+
+This feature is enabled by default and can be disabled
+via link:config-gerrit.html#submodule.enableSuperProjectSubscriptions[submodule.enableSuperProjectSubscriptions]
+in the server configuration.
+
+== Git submodules overview
+
+Submodules are a Git feature that allows an external repository to be
 attached inside a repository at a specific path. The objective here
 is to provide a brief overview, further details can be found
-in the official git submodule command documentation.
+in the official Git submodule documentation.
 
-Imagine a repository called 'super' and another one called 'a'.
-Also consider 'a' available in a running Gerrit instance on "server".
-With this feature, one could attach 'a' inside of 'super' repository
-at path 'a' by executing the following command when being inside
+Imagine a repository called 'super' and another one called 'sub'.
+Also consider 'sub' available in a running Gerrit instance on "server".
+With this feature, one could attach 'sub' inside of 'super' repository
+at path 'sub' by executing the following command when being inside
 'super':
 =====
-git submodule add ssh://server/a a
+git submodule add ssh://server/sub sub
 =====
 
 Still considering the above example, after its execution notice that
-inside the local repository 'super' the 'a' folder is considered a
-gitlink to the external repository 'a'. Also notice a file called
+inside the local repository 'super' the 'sub' folder is considered a
+gitlink to the external repository 'sub'. Also notice a file called
 .gitmodules is created (it is a configuration file containing the
-subscription of 'a'). To provide the SHA-1 each gitlink points to in
+subscription of 'sub'). To provide the SHA-1 each gitlink points to in
 the external repository, one should use the command:
 ====
 git submodule status
 ====
 
-In the example provided, if 'a' is updated and 'super' is supposed
-to see the latest SHA-1 (considering here 'a' has only the master
-branch), one should then commit the modified gitlink for 'a' in
+In the example provided, if 'sub' is updated and 'super' is supposed
+to see the latest SHA-1 (considering here 'sub' has only the master
+branch), one should then commit the modified gitlink for 'sub' in
 the 'super' project. Actually it would not even need to be an
-external update, one could move to 'a' folder (insider 'super'),
+external update, one could move to 'sub' folder (inside 'super'),
 modify its content, commit, then move back to 'super' and
-commit the modified gitlink for 'a'.
+commit the modified gitlink for 'sub'.
 
-== Creating a New Subscription
+== Creating a new subscription
 
-=== Defining the Submodule Branch
+=== Ensure the subscription is allowed
 
-This is required because submodule subscription is actually the
-subscription of a submodule project and one of its branches for
-a branch of a super project.
+Gerrit has a complex access control system, where different repositories
+can be accessed by different groups of people. To ensure that the submodule
+related information is allowed to be exposed in the superproject,
+the submodule needs to be configured to enable the superproject subscription.
+In a submodule client, checkout the refs/meta/config branch and edit
+the subscribe capabilities in the 'project.config' file:
+====
+    git fetch <remote> refs/meta/config:refs/meta/config
+    git checkout refs/meta/config
+    $EDITOR project.config
+====
+and add the following lines:
+====
+  [subscribe "<superproject>"]
+    refs = <refspec>
+====
+where the 'superproject' should be the exact project name of the superproject.
+The refspec defines which branches of the submodule are allowed to be
+subscribed to which branches of the superproject. See below for
+link:#acl_refspec[details]. Push the configuration for review and
+submit the change:
+====
+  git add project.config
+  git commit -m "Allow <superproject> to subscribe"
+  git push <remote> HEAD:refs/for/refs/meta/config
+====
+After the change is integrated a superproject subscription is possible.
+
+=== Defining the submodule branch
 
 Since Gerrit manages subscriptions in the branch scope, we could have
 a scenario having a project called 'super' having a branch 'integration'
-subscribed to a project called 'a' in branch 'integration', and also
-having the same 'super' project but in branch 'dev' subscribed to the 'a'
+subscribed to a project called 'sub' in branch 'integration', and also
+having the same 'super' project but in branch 'dev' subscribed to the 'sub'
 project in a branch called 'local-dev'.
 
 After adding the git submodule to a super project, one should edit
 the .gitmodules file to add a branch field to each submodule
 section which is supposed to be subscribed.
 
-As the branch field is a Gerrit specific field it will not be filled
+As the branch field is a Gerrit-specific field it will not be filled
 automatically by the git submodule command, so one needs to edit it
 manually. Its value should indicate the branch of a submodule project
 that when updated will trigger automatic update of its registered
@@ -90,28 +130,38 @@
 .gitmodules file, Gerrit will not create a subscription for the
 submodule and there will be no automatic updates to the superproject.
 
-=== Detecting and Subscribing Submodules
+Whenever a commit is merged to a project, its project config is checked
+to see if any potential superprojects are allowed to subscribe to it.
+If so, the superproject is checked if a valid subscription exists
+by checking the .gitmodules file for the a submodule which includes
+a `branch` field and a url pointing to this server.
 
-Whenever a commit is merged to a project, its content is scanned
-to identify if it registers any submodules (if the commit contains new
-gitlinks and a .gitmodules file with all required info) and if so,
-a new submodule subscription is registered.
+[[acl_refspec]]
+=== The RefSpec in the allowSuperproject section
+The RefSpec for defining the branch level access for subscriptions look similar
+to Git style RefSpecs used for pushing in Git. Regular expressions
+as found in the ACL configuration are not supported. The most restrictive
+RefSpec is allowing one specific branch of the submodule to be subscribed
+to one specific branch of the superproject via:
+====
+  [allowSuperproject "<superproject>"]
+    refs = refs/heads/<submodule-branch>:refs/heads/<superproject-branch>
+====
 
-[[automatic_update]]
-== Automatic Update of Superprojects
+If you want to allow for a 1:1 mapping, i.e. 'master' maps to 'master',
+'stable' maps to 'stable', but not allowing 'master' to be subscribed to
+'stable':
+====
+  [allowSuperproject "<superproject>"]
+    refs/heads/*:refs/heads/*
+====
 
-After a superproject is subscribed to a submodule, it is not
-required to push/merge commits to this superproject to update the
-gitlink to the submodule.
-
-Whenever a commit is merged in a submodule, its subscribed superproject
-is updated.
-
-Imagine a superproject called 'super' having a branch called 'dev'
-having subscribed to a submodule 'a' on a branch 'dev-of-a'. When a commit
-is merged in branch 'dev-of-a' of 'a' project, Gerrit automatically
-creates a new commit on branch 'dev' of 'super' updating the gitlink
-to point to the just merged commit.
+If you want to enable a branch to be subscribed to any other branch of
+the superproject, omit the second part of the RefSpec:
+====
+  [allowSuperproject "<superproject>"]
+    refs/heads/<submodule-branch>
+====
 
 === Subscription Limitations
 
@@ -119,7 +169,7 @@
 submodules are hosted on the same Gerrit instance as the
 superproject. Gerrit determines this by checking the hostname of the
 submodule specified in the .gitmodules file and comparing it to the
-hostname from the canonical web URL.
+hostname from the link:config-gerrit.html#gerrit.canonicalWebUrl[`gerrit.canonicalWebUrl`].
 
 It is currently not possible to use the submodule subscription feature
 with a canonical web URL hostname that differs from the hostname of
@@ -170,10 +220,9 @@
 
 == Removing Subscriptions
 
-If one has added a submodule subscription and drops it, it is
-required to merge a commit updating the subscribed super
-project/branch to remove the gitlink and the submodule section
-of the .gitmodules file.
+To remove a subscription, either disable the subscription from the
+submodules configuration or remove the submodule or information thereof
+(such as the branch field) in the superproject.
 
 GERRIT
 ------
diff --git a/README.md b/README.md
index 001e1bd..b2a867f 100644
--- a/README.md
+++ b/README.md
@@ -71,7 +71,3 @@
 
 _NOTE: release is optional. Last released package of the version is installed if the release
 number is omitted._
-
-## Events
-
-- March 14-18 2016: Gerrit Hackathon, Berlin (free seats are still available).
diff --git a/ReleaseNotes/ReleaseNotes-2.12.2.txt b/ReleaseNotes/ReleaseNotes-2.12.2.txt
index 5582bf9..500b015 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.2.txt
@@ -6,7 +6,42 @@
 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].
+Schema Upgrade
+--------------
+
+*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.1.html[
+2.12.1] but a manual schema upgrade is necessary 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, or from 2.12.1 having already
+done the migration, this manual step is not necessary and should be omitted.
+
 
 Bug Fixes
 ---------
diff --git a/contrib/bash_completion b/contrib/bash_completion
index 6772235..19060a5c 100644
--- a/contrib/bash_completion
+++ b/contrib/bash_completion
@@ -65,7 +65,7 @@
     COMPREPLY=()
     cur="${COMP_WORDS[COMP_CWORD]}"
     prev="${COMP_WORDS[COMP_CWORD-1]}"
-    opts="check restart run start status stop supervise"
+    opts="check restart run start status stop supervise threads"
 
     COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
 }
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index e3dd423..9920c69 100644
--- a/gerrit-acceptance-framework/pom.xml
+++ b/gerrit-acceptance-framework/pom.xml
@@ -5,7 +5,7 @@
   <version>2.13-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
-  <description>API for Gerrit Plugins</description>
+  <description>Framework for Gerrit's acceptance tests</description>
   <url>https://www.gerritcodereview.com/</url>
 
   <licenses>
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 046008c..9cf1515 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -17,7 +17,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
-import static com.google.gerrit.testutil.GerritServerTests.isNoteDbTestEnabled;
 
 import com.google.common.base.Function;
 import com.google.common.base.Optional;
@@ -32,6 +31,8 @@
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.projects.BranchApi;
+import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -41,6 +42,7 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -288,7 +290,7 @@
     }
 
     server.getTestInjector().injectMembers(this);
-    notesMigration.setAllEnabled(isNoteDbTestEnabled());
+    notesMigration.setFromEnv();
     Transport.register(inProcessProtocol);
     toClose = Collections.synchronizedList(new ArrayList<Repository>());
     admin = accounts.admin();
@@ -460,6 +462,13 @@
     return pushTo("refs/drafts/master");
   }
 
+  protected BranchApi createBranch(Branch.NameKey branch) throws Exception {
+    return gApi.projects()
+        .name(branch.getParentKey().get())
+        .branch(branch.get())
+        .create(new BranchInput());
+  }
+
   private static final List<Character> RANDOM =
       Chars.asList(new char[]{'a','b','c','d','e','f','g','h'});
   protected PushOneCommit.Result amendChange(String changeId)
@@ -476,6 +485,11 @@
     return push.to(ref);
   }
 
+  protected void merge(PushOneCommit.Result r) throws Exception {
+    revision(r).review(ReviewInput.approve());
+    revision(r).submit();
+  }
+
   protected ChangeInfo info(String id)
       throws RestApiException {
     return gApi.changes().id(id).info();
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 238567c..08977f0 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.acceptance;
 
-import static com.google.common.base.Preconditions.checkState;
-
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
@@ -30,8 +28,8 @@
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.testutil.FakeEmailSender;
-import com.google.gerrit.testutil.GerritServerTests;
 import com.google.gerrit.testutil.NoteDbChecker;
+import com.google.gerrit.testutil.NoteDbMode;
 import com.google.gerrit.testutil.TempFileUtil;
 import com.google.inject.Injector;
 import com.google.inject.Key;
@@ -286,11 +284,9 @@
 
   void stop() throws Exception {
     try {
-      if (GerritServerTests.isEnvVarTrue("GERRIT_CHECK_NOTEDB")) {
-        checkState(!GerritServerTests.isNoteDbTestEnabled(),
-            "cannot rebuild and check NoteDb when starting from scratch with"
-                + " NoteDb enabled");
-        testInjector.getInstance(NoteDbChecker.class).checkAllChanges();
+      if (NoteDbMode.get().equals(NoteDbMode.CHECK)) {
+        testInjector.getInstance(NoteDbChecker.class)
+            .rebuildAndCheckAllChanges();
       }
     } finally {
       daemon.getLifecycleManager().stop();
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 291b953..84e7557 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
@@ -77,6 +77,12 @@
         ReviewDb db,
         PersonIdent i,
         TestRepository<?> testRepo,
+        @Assisted("changeId") String changeId);
+
+    PushOneCommit create(
+        ReviewDb db,
+        PersonIdent i,
+        TestRepository<?> testRepo,
         @Assisted("subject") String subject,
         @Assisted("fileName") String fileName,
         @Assisted("content") String content);
@@ -143,6 +149,18 @@
       @Assisted ReviewDb db,
       @Assisted PersonIdent i,
       @Assisted TestRepository<?> testRepo,
+      @Assisted("changeId") String changeId) throws Exception {
+    this(notesFactory, approvalsUtil, queryProvider,
+        db, i, testRepo, SUBJECT, FILE_NAME, FILE_CONTENT, changeId);
+  }
+
+  @AssistedInject
+  PushOneCommit(ChangeNotes.Factory notesFactory,
+      ApprovalsUtil approvalsUtil,
+      Provider<InternalChangeQuery> queryProvider,
+      @Assisted ReviewDb db,
+      @Assisted PersonIdent i,
+      @Assisted TestRepository<?> testRepo,
       @Assisted("subject") String subject,
       @Assisted("fileName") String fileName,
       @Assisted("content") String content) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index ddb1b60..cf866e1 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -26,7 +26,6 @@
 import static com.google.gerrit.server.project.Util.blockLabel;
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
-import static com.google.gerrit.testutil.GerritServerTests.isNoteDbTestEnabled;
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableSet;
@@ -68,6 +67,7 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
+import com.google.gerrit.testutil.NoteDbMode;
 
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -525,11 +525,11 @@
         .id(r.getChangeId())
         .get();
 
-    // When notedb is enabled adding a reviewer records that user as reviewer
-    // in notedb. When notedb is disabled adding a reviewer results in a dummy 0
+    // When NoteDb is enabled adding a reviewer records that user as reviewer
+    // in NoteDb. When NoteDb is disabled adding a reviewer results in a dummy 0
     // approval on the change which is treated as CC when the ChangeInfo is
     // created.
-    Collection<AccountInfo> reviewers = isNoteDbTestEnabled()
+    Collection<AccountInfo> reviewers = NoteDbMode.readWrite()
         ? c.reviewers.get(REVIEWER)
         : c.reviewers.get(CC);
     assertThat(reviewers).isNotNull();
@@ -574,9 +574,9 @@
         .id(r.getChangeId())
         .get();
     reviewers = c.reviewers.get(REVIEWER);
-    if (isNoteDbTestEnabled()) {
-      // When notedb is enabled adding a reviewer records that user as reviewer
-      // in notedb.
+    if (NoteDbMode.readWrite()) {
+      // When NoteDb is enabled adding a reviewer records that user as reviewer
+      // in NoteDb.
       assertThat(reviewers).hasSize(2);
       Iterator<AccountInfo> reviewerIt = reviewers.iterator();
       assertThat(reviewerIt.next()._accountId)
@@ -585,7 +585,7 @@
           .isEqualTo(user.getId().get());
       assertThat(c.reviewers).doesNotContainKey(CC);
     } else {
-      // When notedb is disabled adding a reviewer results in a dummy 0 approval
+      // When NoteDb is disabled adding a reviewer results in a dummy 0 approval
       // on the change which is treated as CC when the ChangeInfo is created.
       assertThat(reviewers).hasSize(1);
       assertThat(reviewers.iterator().next()._accountId)
@@ -653,14 +653,14 @@
         .reviewer(user.getId().toString())
         .votes();
 
-    if (isNoteDbTestEnabled()) {
-      // When notedb is enabled each reviewer is explicitly recorded in the
-      // notedb and this record stays even when all votes of that user have been
+    if (NoteDbMode.readWrite()) {
+      // When NoteDb is enabled each reviewer is explicitly recorded in the
+      // NoteDb and this record stays even when all votes of that user have been
       // deleted, hence there is no dummy 0 approval left when a vote is
       // deleted.
       assertThat(m).isEmpty();
     } else {
-      // When notedb is disabled there is a dummy 0 approval on the change so
+      // When NoteDb is disabled there is a dummy 0 approval on the change so
       // that the user is still returned as CC when all votes of that user have
       // been deleted.
       assertThat(m).containsEntry("Code-Review", new Short((short)0));
@@ -674,15 +674,15 @@
     assertThat(message.author._accountId).isEqualTo(admin.getId().get());
     assertThat(message.message).isEqualTo(
         "Removed Code-Review+1 by User <user@example.com>\n");
-    if (isNoteDbTestEnabled()) {
-      // When notedb is enabled each reviewer is explicitly recorded in the
-      // notedb and this record stays even when all votes of that user have been
+    if (NoteDbMode.readWrite()) {
+      // When NoteDb is enabled each reviewer is explicitly recorded in the
+      // NoteDb and this record stays even when all votes of that user have been
       // deleted.
       assertThat(getReviewers(c.reviewers.get(REVIEWER)))
           .containsExactlyElementsIn(
               ImmutableSet.of(admin.getId(), user.getId()));
     } else {
-      // When notedb is disabled users that have only dummy 0 approvals on the
+      // When NoteDb is disabled users that have only dummy 0 approvals on the
       // change are returned as CC and not as REVIEWER.
       assertThat(getReviewers(c.reviewers.get(REVIEWER)))
           .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
@@ -892,7 +892,7 @@
 
   @Test
   public void check() throws Exception {
-    // TODO(dborowitz): Re-enable when ConsistencyChecker supports notedb.
+    // TODO(dborowitz): Re-enable when ConsistencyChecker supports NoteDb.
     assume().that(notesMigration.enabled()).isFalse();
     PushOneCommit.Result r = createChange();
     assertThat(gApi.changes()
@@ -1067,7 +1067,7 @@
   }
 
   @Test
-  public void notedbCommitsOnPatchSetCreation() throws Exception {
+  public void noteDbCommitsOnPatchSetCreation() throws Exception {
     assume().that(notesMigration.enabled()).isTrue();
 
     PushOneCommit.Result r = createChange();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index def8317..24cbac4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -716,11 +716,6 @@
     oldETag = checkETag(getRevisionActions, r2, oldETag);
   }
 
-  private void merge(PushOneCommit.Result r) throws Exception {
-    revision(r).review(ReviewInput.approve());
-    revision(r).submit();
-  }
-
   private PushOneCommit.Result updateChange(PushOneCommit.Result r,
       String content) throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
index 53412cb..24ddbf2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -18,7 +18,10 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
 
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -69,6 +72,27 @@
     pushSubmoduleConfig(repo, branch, config);
   }
 
+  protected void allowSubmoduleSubscription(String submodule, String subBranch,
+      String superproject, String superBranch) throws Exception {
+    Project.NameKey sub = new Project.NameKey(name(submodule));
+    Project.NameKey superName = new Project.NameKey(name(superproject));
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(sub)) {
+      md.setMessage("Added superproject subscription");
+      ProjectConfig pc = ProjectConfig.read(md);
+      SubscribeSection s = new SubscribeSection(superName);
+      if (superBranch == null) {
+        s.addRefSpec(subBranch);
+      } else {
+        s.addRefSpec(subBranch + ":" + superBranch);
+      }
+      pc.addSubscribeSection(s);
+      ObjectId oldId = pc.getRevision();
+      ObjectId newId = pc.commit(md);
+      assertThat(newId).isNotEqualTo(oldId);
+      projectCache.evict(pc.getProject());
+    }
+  }
+
   protected void prepareSubmoduleConfigEntry(Config config,
       String subscribeToRepo, String subscribeToBranch) {
     subscribeToRepo = name(subscribeToRepo);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index 2f3baae..e69a647 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -29,11 +29,25 @@
 public class SubmoduleSubscriptionsIT extends AbstractSubmoduleSubscription {
 
   @Test
-  public void testSubscriptionToEmptyRepo() throws Exception {
+  @GerritConfig(name = "submodule.enableSuperProjectSubscriptions", value = "false")
+  public void testSubscriptionWithoutServerSetting() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
 
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+  }
+
+  @Test
+  public void testSubscriptionToEmptyRepo() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
+
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
     ObjectId subHEAD = pushChangeTo(subRepo, "master");
     expectToHaveSubmoduleState(superRepo, "master",
         "subscribed-to-project", subHEAD);
@@ -43,6 +57,8 @@
   public void testSubscriptionToExistingRepo() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -52,10 +68,68 @@
   }
 
   @Test
+  public void testSubscriptionWildcardACLForSingleBranch() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    // master is allowed to be subscribed to any superprojects branch:
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", null);
+    // create 'branch':
+    pushChangeTo(superRepo, "branch");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "master");
+
+    ObjectId subHEAD = pushChangeTo(subRepo, "master");
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEAD);
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD);
+  }
+
+  @Test
+  public void testSubscriptionWildcardACLOneOnOneMapping() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    // any branch is allowed to be subscribed to the same superprojects branch:
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/*",
+        "super-project", "refs/heads/*");
+
+    // create 'branch' in both repos:
+    pushChangeTo(superRepo, "branch");
+    pushChangeTo(subRepo, "branch");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    createSubmoduleSubscription(superRepo, "branch", "subscribed-to-project", "branch");
+
+    ObjectId subHEAD1 = pushChangeTo(subRepo, "master");
+    ObjectId subHEAD2 = pushChangeTo(subRepo, "branch");
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEAD1);
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD2);
+
+    // Now test that cross subscriptions do not work:
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "branch");
+    ObjectId subHEAD3 = pushChangeTo(subRepo, "branch");
+
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEAD1);
+    expectToHaveSubmoduleState(superRepo, "branch",
+        "subscribed-to-project", subHEAD3);
+  }
+
+  @Test
   @GerritConfig(name = "submodule.verboseSuperprojectUpdate", value = "false")
   public void testSubmoduleShortCommitMessage() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -76,10 +150,11 @@
   }
 
   @Test
-
   public void testSubmoduleCommitMessage() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -108,6 +183,8 @@
   public void testSubscriptionUnsubscribe() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -131,6 +208,8 @@
   public void testSubscriptionUnsubscribeByDeletingGitModules() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -154,6 +233,8 @@
   public void testSubscriptionToDifferentBranches() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/foo",
+        "super-project", "refs/heads/master");
 
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "foo");
     ObjectId subFoo = pushChangeTo(subRepo, "foo");
@@ -167,6 +248,10 @@
   public void testCircularSubscriptionIsDetected() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
+    allowSubmoduleSubscription("super-project", "refs/heads/master",
+        "subscribed-to-project", "refs/heads/master");
 
     pushChangeTo(subRepo, "master");
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
@@ -181,6 +266,44 @@
     assertThat(hasSubmodule(subRepo, "master", "super-project")).isFalse();
   }
 
+
+  @Test
+  public void testSubscriptionFailOnMissingACL() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+  }
+
+  @Test
+  public void testSubscriptionFailOnWrongProjectACL() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "wrong-super-project", "refs/heads/master");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+  }
+
+  @Test
+  public void testSubscriptionFailOnWrongBranchACL() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/wrong-branch");
+
+    pushChangeTo(subRepo, "master");
+    createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
+    pushChangeTo(subRepo, "master");
+    assertThat(hasSubmodule(superRepo, "master", "subscribed-to-project")).isFalse();
+  }
+
   private void deleteAllSubscriptions(TestRepository<?> repo, String branch)
       throws Exception {
     repo.git().fetch().setRemote("origin").call();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index e4a054a..30482dd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -41,6 +41,9 @@
   public void testSubscriptionUpdateOfManyChanges() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+    allowSubmoduleSubscription("subscribed-to-project", "refs/heads/master",
+        "super-project", "refs/heads/master");
+
     createSubmoduleSubscription(superRepo, "master", "subscribed-to-project", "master");
 
     ObjectId subHEAD = subRepo.branch("HEAD").commit().insertChangeId()
@@ -96,6 +99,13 @@
     TestRepository<?> sub2 = createProjectWithPush("sub2");
     TestRepository<?> sub3 = createProjectWithPush("sub3");
 
+    allowSubmoduleSubscription("sub1", "refs/heads/master",
+        "super-project", "refs/heads/master");
+    allowSubmoduleSubscription("sub2", "refs/heads/master",
+        "super-project", "refs/heads/master");
+    allowSubmoduleSubscription("sub3", "refs/heads/master",
+        "super-project", "refs/heads/master");
+
     Config config = new Config();
     prepareSubmoduleConfigEntry(config, "sub1", "master");
     prepareSubmoduleConfigEntry(config, "sub2", "master");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
index 248ea02..176cf49 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
@@ -256,8 +256,8 @@
   /**
    * Assert that refs seen by a non-admin user match expected.
    *
-   * @param expectedWithMeta expected refs, in order. If notedb is disabled by
-   *     the configuration, any notedb refs (i.e. ending in "/meta") are removed
+   * @param expectedWithMeta expected refs, in order. If NoteDb is disabled by
+   *     the configuration, any NoteDb refs (i.e. ending in "/meta") are removed
    *     from the expected list before comparing to the actual results.
    * @throws Exception
    */
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java
similarity index 95%
rename from gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
rename to gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java
index b3bb28c..66e0c73 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNotedbIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java
@@ -28,7 +28,7 @@
 
 import java.io.File;
 
-public class RebuildNotedbIT {
+public class RebuildNoteDbIT {
   private File sitePath;
 
   @Before
@@ -49,7 +49,7 @@
     Files.append(ConfigNotesMigration.allEnabledConfig().toText(),
         new File(sitePath.toString(), "etc/gerrit.config"),
         UTF_8);
-    runGerrit("RebuildNotedb", "-d", sitePath.toString(),
+    runGerrit("RebuildNoteDb", "-d", sitePath.toString(),
         "--show-stack-trace");
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index adf67f1..a714f6e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -115,7 +115,7 @@
   }
 
   @Test
-  public void notedbCommit() throws Exception {
+  public void noteDbCommit() throws Exception {
     assume().that(notesMigration.enabled()).isTrue();
 
     ChangeInfo c = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
index 07e1592..bcbec9a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.testutil.GerritServerTests.isNoteDbTestEnabled;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -32,6 +31,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.NoteDbMode;
 
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
@@ -129,7 +129,7 @@
     assertThat(label.all.get(0)._accountId).isEqualTo(user.id.get());
     assertThat(label.all.get(0).value).isEqualTo(0);
 
-    ReviewerState rs = isNoteDbTestEnabled()
+    ReviewerState rs = NoteDbMode.readWrite()
         ? ReviewerState.REVIEWER : ReviewerState.CC;
     Collection<AccountInfo> ccs = info.reviewers.get(rs);
     assertThat(ccs).hasSize(1);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
new file mode 100644
index 0000000..df1ebfd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -0,0 +1,276 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.MoveInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
+
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+@NoHttpd
+public class MoveChangeIT extends AbstractDaemonTest {
+  @Test
+  public void moveChange_shortRef() throws Exception {
+    // Move change to a different branch using short ref name
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    move(r.getChangeId(), newBranch.getShortName());
+    assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
+  }
+
+  @Test
+  public void moveChange_fullRef() throws Exception {
+    // Move change to a different branch using full ref name
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    move(r.getChangeId(), newBranch.get());
+    assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
+  }
+
+  @Test
+  public void moveChangeWithMessage() throws Exception {
+    // Provide a message using --message flag
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    String moveMessage = "Moving for the move test";
+    move(r.getChangeId(), newBranch.get(), moveMessage);
+    assertThat(r.getChange().change().getDest()).isEqualTo(newBranch);
+    StringBuilder expectedMessage = new StringBuilder();
+    expectedMessage.append("Change destination moved from master to moveTest");
+    expectedMessage.append("\n\n");
+    expectedMessage.append(moveMessage);
+    assertThat(r.getChange().messages().get(1).getMessage())
+        .isEqualTo(expectedMessage.toString());
+  }
+
+  @Test
+  public void moveChangeToSameRefAsCurrent() throws Exception {
+    // Move change to the branch same as change's destination
+    PushOneCommit.Result r = createChange();
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Change is already destined for the specified branch");
+    move(r.getChangeId(), r.getChange().change().getDest().get());
+  }
+
+  @Test
+  public void moveChange_sameChangeId() throws Exception {
+    // Move change to a branch with existing change with same change ID
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    int changeNum = r.getChange().change().getChangeId();
+    createChange(newBranch.get(), r.getChangeId());
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Destination " + newBranch.getShortName()
+        + " has a different change with same change key " + r.getChangeId());
+    move(changeNum, newBranch.get());
+  }
+
+  @Test
+  public void moveChangeToNonExistentRef() throws Exception {
+    // Move change to a non-existing branch
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch = new Branch.NameKey(
+        r.getChange().change().getProject(), "does_not_exist");
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Destination " + newBranch.get()
+        + " not found in the project");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveClosedChange() throws Exception {
+    // Move a change which is not open
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    merge(r);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Change is merged");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveMergeCommitChange() throws Exception {
+    // Move a change which has a merge commit as the current PS
+    // Create a merge commit and push for review
+    PushOneCommit.Result r1 = createChange();
+    PushOneCommit.Result r2 = createChange();
+    TestRepository<?>.CommitBuilder commitBuilder =
+        testRepo.branch("HEAD").commit().insertChangeId();
+    commitBuilder
+      .parent(r1.getCommit())
+      .parent(r2.getCommit())
+      .message("Move change Merge Commit")
+      .author(admin.getIdent())
+      .committer(new PersonIdent(admin.getIdent(), testRepo.getDate()));
+    RevCommit c = commitBuilder.create();
+    pushHead(testRepo, "refs/for/master", false, false);
+
+    // Try to move the merge commit to another branch
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r1.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Merge commit cannot be moved");
+    move(GitUtil.getChangeId(testRepo, c).get(), newBranch.get());
+  }
+
+  @Test
+  public void moveChangeToBranch_WithoutUploadPerms() throws Exception {
+    // Move change to a destination where user doesn't have upload permissions
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "blocked_branch");
+    createBranch(newBranch);
+    block(Permission.PUSH,
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
+        "refs/for/" + newBranch.get());
+    exception.expect(AuthException.class);
+    exception.expectMessage("Move not permitted");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveChangeFromBranch_WithoutAbandonPerms() throws Exception {
+    // Move change for which user does not have abandon permissions
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+    block(Permission.ABANDON,
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
+        r.getChange().change().getDest().get());
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("Move not permitted");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  @Test
+  public void moveChangeToBranchThatContainsCurrentCommit() throws Exception {
+    // Move change to a branch for which current PS revision is reachable from
+    // tip
+
+    // Create a change
+    PushOneCommit.Result r = createChange();
+    int changeNum = r.getChange().change().getChangeId();
+
+    // Create a branch with that same commit
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    BranchInput bi = new BranchInput();
+    bi.revision = r.getCommit().name();
+    gApi.projects()
+      .name(newBranch.getParentKey().get())
+      .branch(newBranch.get())
+      .create(bi);
+
+    // Try to move the change to the branch with the same commit
+    exception.expect(ResourceConflictException.class);
+    exception
+        .expectMessage("Current patchset revision is reachable from tip of "
+            + newBranch.get());
+    move(changeNum, newBranch.get());
+  }
+
+  @Test
+  public void moveChange_WithCurrentPatchSetLocked() throws Exception {
+    // Move change that is locked
+    PushOneCommit.Result r = createChange();
+    Branch.NameKey newBranch =
+        new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
+    createBranch(newBranch);
+
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    LabelType patchSetLock = Util.patchSetLock();
+    cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock);
+    AccountGroup.UUID registeredUsers =
+        SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+    Util.allow(cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, registeredUsers,
+        "refs/heads/*");
+    saveProjectConfig(cfg);
+    grant(Permission.LABEL + "Patch-Set-Lock", project, "refs/heads/*");
+    revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
+
+    exception.expect(AuthException.class);
+    exception.expectMessage("Move not permitted");
+    move(r.getChangeId(), newBranch.get());
+  }
+
+  private void saveProjectConfig(ProjectConfig cfg) throws Exception {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      cfg.commit(md);
+    }
+  }
+
+  private void move(int changeNum, String destination)
+      throws RestApiException {
+    gApi.changes().id(changeNum).move(destination);
+  }
+
+  private void move(String changeId, String destination)
+      throws RestApiException {
+    gApi.changes().id(changeId).move(destination);
+  }
+
+  private void move(String changeId, String destination, String message)
+      throws RestApiException {
+    MoveInput in = new MoveInput();
+    in.destination_branch = destination;
+    in.message = message;
+    gApi.changes().id(changeId).move(in);
+  }
+
+  private PushOneCommit.Result createChange(String branch, String changeId)
+      throws Exception {
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, changeId);
+    PushOneCommit.Result result = push.to("refs/for/" + branch);
+    result.assertOkStatus();
+    return result;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index d1f2bfe..b7a6b93 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -52,6 +52,8 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 @NoHttpd
 public class CommentsIT extends AbstractDaemonTest {
@@ -375,6 +377,8 @@
 
     addDraft(r1.getChangeId(), r1.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace"));
+    addDraft(r1.getChangeId(), r1.getCommit().getName(),
+        newDraft(FILE_NAME, Side.PARENT, 2, "what happened to this?"));
     addDraft(r2.getChangeId(), r2.getCommit().getName(),
         newDraft(FILE_NAME, Side.REVISION, 1, "join lines"));
     addDraft(r2.getChangeId(), r2.getCommit().getName(),
@@ -410,8 +414,11 @@
         .comments();
     assertThat(ps1Map.keySet()).containsExactly(FILE_NAME);
     List<CommentInfo> ps1List = ps1Map.get(FILE_NAME);
-    assertThat(ps1List).hasSize(1);
-    assertThat(ps1List.get(0).message).isEqualTo("nit: trailing whitespace");
+    assertThat(ps1List).hasSize(2);
+    assertThat(ps1List.get(0).message).isEqualTo("what happened to this?");
+    assertThat(ps1List.get(0).side).isEqualTo(Side.PARENT);
+    assertThat(ps1List.get(1).message).isEqualTo("nit: trailing whitespace");
+    assertThat(ps1List.get(1).side).isNull();
 
     assertThat(gApi.changes()
           .id(r2.getChangeId())
@@ -433,17 +440,20 @@
     assertThat(messages).hasSize(1);
     String url = canonicalWebUrl.get();
     int c = r1.getChange().getId().get();
-    assertThat(messages.get(0).body()).contains(
-        "\n"
-        + "Patch Set 2:\n"
+    assertThat(extractComments(messages.get(0).body())).isEqualTo(
+        "Patch Set 2:\n"
         + "\n"
-        + "(3 comments)\n"
+        + "(4 comments)\n"
         + "\n"
         + "comments\n"
         + "\n"
         + url + "#/c/" + c + "/1/a.txt\n"
         + "File a.txt:\n"
         + "\n"
+        + "PS1, Line 2: \n"
+        + "what happened to this?\n"
+        + "\n"
+        + "\n"
         + "PS1, Line 1: ew\n"
         + "nit: trailing whitespace\n"
         + "\n"
@@ -457,9 +467,14 @@
         + "\n"
         + "PS2, Line 2: nten\n"
         + "typo: content\n"
-        + "\n"
-        + "\n"
-        + "-- \n");
+        + "\n");
+  }
+
+  private static String extractComments(String msg) {
+    // Extract lines between start "....." and end "-- ".
+    Pattern p = Pattern.compile(".*[.]{5}\n+(.*)\\n+-- \n.*", Pattern.DOTALL);
+    Matcher m = p.matcher(msg);
+    return m.matches() ? m.group(1) : msg;
   }
 
   private void addComment(PushOneCommit.Result r, String message)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index df4772a..f846151 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -72,7 +72,7 @@
 
   @Before
   public void setUp() throws Exception {
-    // TODO(dborowitz): Re-enable when ConsistencyChecker supports notedb.
+    // TODO(dborowitz): Re-enable when ConsistencyChecker supports NoteDb.
     // Note that we *do* want to enable these tests with GERRIT_CHECK_NOTEDB, as
     // we need to be able to convert old, corrupt changes. However, those tests
     // don't necessarily need to pass.
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index cf29897..835fba5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.acceptance.server.notedb;
 
 import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.testutil.GerritServerTests.isNoteDbTestEnabled;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.change.Rebuild;
 import com.google.gerrit.testutil.NoteDbChecker;
+import com.google.gerrit.testutil.NoteDbMode;
 import com.google.inject.Inject;
 
 import org.junit.Before;
@@ -30,9 +32,12 @@
   @Inject
   private NoteDbChecker checker;
 
+  @Inject
+  private Rebuild rebuildHandler;
+
   @Before
   public void setUp() {
-    assume().that(isNoteDbTestEnabled()).isFalse();
+    assume().that(NoteDbMode.readWrite()).isFalse();
     notesMigration.setAllEnabled(false);
   }
 
@@ -41,7 +46,7 @@
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getPatchSetId().getParentKey();
     gApi.changes().id(id.get()).topic(name("a-topic"));
-    checker.checkChanges(id);
+    checker.rebuildAndCheckChanges(id);
   }
 
   @Test
@@ -49,6 +54,67 @@
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getPatchSetId().getParentKey();
     r = amendChange(r.getChangeId());
+    checker.rebuildAndCheckChanges(id);
+  }
+
+  @Test
+  public void noWriteToNewRef() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    checker.assertNoChangeRef(project, id);
+
+    notesMigration.setWriteChanges(true);
+    gApi.changes().id(id.get()).topic(name("a-topic"));
+
+    // First write doesn't create the ref, but rebuilding works.
+    checker.assertNoChangeRef(project, id);
+    checker.rebuildAndCheckChanges(id);
+
+    // Now that there is a ref, writes are "turned on" for this change, and
+    // NoteDb stays up to date without explicit rebuilding.
+    gApi.changes().id(id.get()).topic(name("new-topic"));
     checker.checkChanges(id);
   }
+
+  @Test
+  public void restApiNotFoundWhenNoteDbDisabled() throws Exception {
+    PushOneCommit.Result r = createChange();
+    exception.expect(ResourceNotFoundException.class);
+    rebuildHandler.apply(
+        parseChangeResource(r.getChangeId()),
+        new Rebuild.Input());
+  }
+
+  @Test
+  public void rebuildViaRestApi() throws Exception {
+    PushOneCommit.Result r = createChange();
+    Change.Id id = r.getPatchSetId().getParentKey();
+    notesMigration.setWriteChanges(true);
+
+    checker.assertNoChangeRef(project, id);
+    rebuildHandler.apply(
+        parseChangeResource(r.getChangeId()),
+        new Rebuild.Input());
+    checker.checkChanges(id);
+  }
+
+  @Test
+  public void writeToNewRefForNewChange() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    Change.Id id1 = r1.getPatchSetId().getParentKey();
+
+    notesMigration.setWriteChanges(true);
+    gApi.changes().id(id1.get()).topic(name("a-topic"));
+    PushOneCommit.Result r2 = createChange();
+    Change.Id id2 = r2.getPatchSetId().getParentKey();
+
+    // Second change was created after NoteDb writes were turned on, so it was
+    // allowed to write to a new ref.
+    checker.checkChanges(id2);
+
+    // First change was created before NoteDb writes were turned on, so its meta
+    // ref doesn't exist until a manual rebuild.
+    checker.assertNoChangeRef(project, id1);
+    checker.rebuildAndCheckChanges(id1);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
index 74487ba..a22b09d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
@@ -422,9 +422,9 @@
     }
   }
 
-  private void merge(PushOneCommit.Result r) throws Exception {
-    revision(r).review(ReviewInput.approve());
-    revision(r).submit();
+  @Override
+  protected void merge(PushOneCommit.Result r) throws Exception {
+    super.merge(r);
     try (Repository repo = repoManager.openRepository(project)) {
       assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(
           r.getCommit());
diff --git a/gerrit-common/BUCK b/gerrit-common/BUCK
index ab62b40..ac36cc7 100644
--- a/gerrit-common/BUCK
+++ b/gerrit-common/BUCK
@@ -12,6 +12,7 @@
   SRC + 'common/FileUtil.java',
   SRC + 'common/IoUtil.java',
   SRC + 'common/TimeUtil.java',
+  SRC + 'common/data/SubscribeSection.java',
 ]
 
 java_library(
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
new file mode 100644
index 0000000..7ec1eda
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubscribeSection.java
@@ -0,0 +1,69 @@
+// 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.data;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+
+import org.eclipse.jgit.transport.RefSpec;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/** Portion of a {@link Project} describing superproject subscription rules. */
+public class SubscribeSection {
+
+  private final List<RefSpec> refSpecs;
+  private final Project.NameKey project;
+
+  public SubscribeSection(Project.NameKey p) {
+    project = p;
+    refSpecs = new ArrayList<>();
+  }
+
+  public void addRefSpec(RefSpec spec) {
+    refSpecs.add(spec);
+  }
+
+  public void addRefSpec(String spec) {
+    refSpecs.add(new RefSpec(spec));
+  }
+
+  public Project.NameKey getProject() {
+    return project;
+  }
+
+  /**
+   * Determines if the <code>branch</code> could trigger a
+   * superproject update as allowed via this subscribe section.
+   *
+   * @param branch the branch to check
+   * @return if the branch could trigger a superproject update
+   */
+  public boolean appliesTo(Branch.NameKey branch) {
+    for (RefSpec r : refSpecs) {
+      if (r.matchSource(branch.get())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public Collection<RefSpec> getRefSpecs() {
+    return Collections.unmodifiableCollection(refSpecs);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index c7912cb..ae5f0b8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -77,6 +77,9 @@
   void restore() throws RestApiException;
   void restore(RestoreInput in) throws RestApiException;
 
+  void move(String destination) throws RestApiException;
+  void move(MoveInput in) throws RestApiException;
+
   /**
    * Create a new change that reverts this change.
    *
@@ -230,6 +233,16 @@
     }
 
     @Override
+    public void move(String destination) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void move(MoveInput in) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ChangeApi revert() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/polygerrit-ui/app/test/fake-app.js b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
similarity index 76%
rename from polygerrit-ui/app/test/fake-app.js
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
index 5ee6915..7e4e5e9 100644
--- a/polygerrit-ui/app/test/fake-app.js
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/MoveInput.java
@@ -12,14 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-'use strict';
+package com.google.gerrit.extensions.api.changes;
 
-/**
- * A stub of the global gr-app element. Use this for testing.
- */
-var app = {
-  accountReady: {
-    then: function(cb) { return cb(); },
-  },
-  loggedIn: false,
-};
+public class MoveInput {
+  public String message;
+  public String destination_branch;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
index 56ebf9d..661d253 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ChangeStatus.java
@@ -42,12 +42,12 @@
    * the uploader publishes the change, it becomes a NEW change.
    * Publishing is a one-way action, a change cannot return to DRAFT status.
    * Draft changes are only visible to the uploader and those explicitly
-   * added as reviewers.
+   * added as reviewers. Note that currently draft changes cannot be abandoned.
    *
    * <p>
    * Changes in the DRAFT state can be moved to:
    * <ul>
-   * <li>{@link #NEW} - when the change is published, it becomes a new change;
+   * <li>{@link #NEW} - when the change is published, it becomes a new change.
    * </ul>
    */
   DRAFT,
@@ -69,6 +69,12 @@
    * Once a change has been abandoned, it cannot be further modified by adding
    * a replacement patch set, and it cannot be merged. Draft comments however
    * may be published, permitting reviewers to send constructive feedback.
+   *
+   * <p>
+   * Changes in the ABANDONED state can be moved to:
+   * <ul>
+   * <li>{@link #NEW} - when the Restore action is used.
+   * </ul>
    */
   ABANDONED
-}
\ No newline at end of file
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
index da7db17..52be977 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -74,15 +74,33 @@
    * @param member type of entry to store.
    */
   public static <T> void itemOf(Binder binder, TypeLiteral<T> member) {
-    @SuppressWarnings("unchecked")
-    Key<DynamicItem<T>> key = (Key<DynamicItem<T>>) Key.get(
-        Types.newParameterizedType(DynamicItem.class, member.getType()));
+    Key<DynamicItem<T>> key = keyFor(member);
     binder.bind(key)
       .toProvider(new DynamicItemProvider<>(member, key))
       .in(Scopes.SINGLETON);
   }
 
   /**
+   * Construct a single {@code DynamicItem<T>} with a fixed value.
+   * <p>
+   * Primarily useful for passing {@code DynamicItem}s to constructors in tests.
+   *
+   * @param member type of item.
+   * @param item item to store.
+   */
+  public static <T> DynamicItem<T> itemOf(Class<T> member, T item) {
+    return new DynamicItem<>(
+        keyFor(TypeLiteral.get(member)),
+        Providers.of(item), "gerrit");
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> Key<DynamicItem<T>> keyFor(TypeLiteral<T> member) {
+    return (Key<DynamicItem<T>>) Key.get(
+        Types.newParameterizedType(DynamicItem.class, member.getType()));
+  }
+
+  /**
    * Bind one implementation as the item using a unique annotation.
    *
    * @param binder a new binder created in the module.
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 2625222..e0a18aa 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,6 +136,12 @@
     if (mask == 0) {
       mask = event.getNativeEvent().getKeyCode();
     }
+    if (event.isControlKeyDown()) {
+      mask |= KeyCommand.M_CTRL;
+    }
+    if (event.isMetaKeyDown()) {
+      mask |= KeyCommand.M_META;
+    }
     return mask;
   }
 }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
index 61f73c0..9dcb111 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/GerritUiExtensionPoint.java
@@ -34,6 +34,6 @@
   PROJECT_INFO_SCREEN_TOP, PROJECT_INFO_SCREEN_BOTTOM;
 
   public enum Key {
-    ACCOUNT_INFO, CHANGE_INFO, PROJECT_NAME
+    ACCOUNT_INFO, CHANGE_INFO, PROJECT_NAME, REVISION_INFO
   }
 }
diff --git a/gerrit-gwtui/BUCK b/gerrit-gwtui/BUCK
index 0f0bde4..43ae8e5 100644
--- a/gerrit-gwtui/BUCK
+++ b/gerrit-gwtui/BUCK
@@ -50,9 +50,6 @@
 java_test(
   name = 'ui_tests',
   srcs = glob(['src/test/java/**/*.java']),
-  resources = glob(['src/test/resources/**/*']) + [
-    'src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml',
-  ],
   deps = [
     ':ui_module',
     '//gerrit-common:client',
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 196fef1..79b4135 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -107,8 +107,18 @@
 import com.google.gwtorm.client.KeyUtil;
 
 public class Dispatcher {
+  public static String toPatch(PatchSet.Id diffBase,
+      PatchSet.Id revision, String fileName) {
+    return toPatch("", diffBase, revision, fileName, null, 0);
+  }
+
+  public static String toPatch(PatchSet.Id diffBase,
+      PatchSet.Id revision, String fileName, DisplaySide side, int line) {
+    return toPatch("", diffBase, revision, fileName, side, line);
+  }
+
   public static String toSideBySide(PatchSet.Id diffBase, Patch.Key id) {
-    return toPatch("", diffBase, id);
+    return toPatch("sidebyside", diffBase, id);
   }
 
   public static String toSideBySide(PatchSet.Id diffBase,
@@ -116,11 +126,6 @@
     return toPatch("sidebyside", diffBase, revision, fileName, null, 0);
   }
 
-  public static String toSideBySide(PatchSet.Id diffBase,
-      PatchSet.Id revision, String fileName, DisplaySide side, int line) {
-    return toPatch("sidebyside", diffBase, revision, fileName, side, line);
-  }
-
   public static String toUnified(PatchSet.Id diffBase,
       PatchSet.Id revision, String fileName) {
     return toPatch("unified", diffBase, revision, fileName, null, 0);
@@ -472,7 +477,7 @@
     }
 
     if ("".equals(panel) || /* DEPRECATED URL */"cm".equals(panel)) {
-      if (preferUnified() || (UserAgent.isPortrait() && UserAgent.isMobile())) {
+      if (preferUnified()) {
         unified(token, baseId, id, side, line);
       } else {
         codemirror(token, baseId, id, side, line, false);
@@ -491,7 +496,8 @@
   }
 
   private static boolean preferUnified() {
-    return DiffView.UNIFIED_DIFF.equals(Gerrit.getUserPreferences().diffView());
+    return DiffView.UNIFIED_DIFF.equals(Gerrit.getUserPreferences().diffView())
+        || (UserAgent.isPortrait() && UserAgent.isMobile());
   }
 
   private static void unified(final String token, final PatchSet.Id baseId,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index 6802a0d..4c8c58d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -105,6 +105,7 @@
   String sectionNavigation();
   String sectionActions();
   String keySearch();
+  String keyEditor();
   String keyHelp();
 
   String sectionJumping();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 83736cd..10d7e1d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -88,6 +88,7 @@
 sectionNavigation = Navigation
 sectionActions = Actions
 keySearch = Search
+keyEditor = Open Inline Editor
 keyHelp = Press '?' to view keyboard shortcuts
 
 sectionJumping = Jumping
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index 294573b..00a9c70 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -278,28 +278,68 @@
           @Override
           public void onSuccess(ChangeInfo info) {
             info.init();
-            addExtensionPoints(info);
+            addExtensionPoints(info, initCurrentRevision(info));
             loadConfigInfo(info, base);
           }
         }));
   }
 
-  private void addExtensionPoints(ChangeInfo change) {
+  private RevisionInfo initCurrentRevision(ChangeInfo info) {
+    info.revisions().copyKeysIntoChildren("name");
+    if (edit != null) {
+      edit.setName(edit.commit().commit());
+      info.setEdit(edit);
+      if (edit.hasFiles()) {
+        edit.files().copyKeysIntoChildren("path");
+      }
+      info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
+      JsArray<RevisionInfo> list = info.revisions().values();
+
+      // Edit is converted to a regular revision (with number = 0) and
+      // added to the list of revisions. Additionally under certain
+      // circumstances change edit is assigned to be the current revision
+      // and is selected to be shown on the change screen.
+      // We have two different strategies to assign edit to the current ps:
+      // 1. revision == null: no revision is selected, so use the edit only
+      //    if it is based on the latest patch set
+      // 2. edit was selected explicitly from ps drop down:
+      //    use the edit regardless of which patch set it is based on
+      if (revision == null) {
+        RevisionInfo.sortRevisionInfoByNumber(list);
+        RevisionInfo rev = list.get(list.length() - 1);
+        if (rev.isEdit()) {
+          info.setCurrentRevision(rev.name());
+        }
+      } else if (revision.equals("edit") || revision.equals("0")) {
+        for (int i = 0; i < list.length(); i++) {
+          RevisionInfo r = list.get(i);
+          if (r.isEdit()) {
+            info.setCurrentRevision(r.name());
+            break;
+          }
+        }
+      }
+    }
+    return resolveRevisionToDisplay(info);
+  }
+
+  private void addExtensionPoints(ChangeInfo change, RevisionInfo rev) {
     addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER,
-        headerExtension, change);
+        headerExtension, change, rev);
     addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS,
-        headerExtensionMiddle, change);
+        headerExtensionMiddle, change, rev);
     addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS,
-        headerExtensionRight, change);
+        headerExtensionRight, change, rev);
     addExtensionPoint(
         GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
-        changeExtension, change);
+        changeExtension, change, rev);
   }
 
   private void addExtensionPoint(GerritUiExtensionPoint extensionPoint,
-      Panel p, ChangeInfo change) {
+      Panel p, ChangeInfo change, RevisionInfo rev) {
     ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint);
     extensionPanel.putObject(GerritUiExtensionPoint.Key.CHANGE_INFO, change);
+    extensionPanel.putObject(GerritUiExtensionPoint.Key.REVISION_INFO, rev);
     p.add(extensionPanel);
   }
 
@@ -375,7 +415,7 @@
     }
   }
 
-  private void gotoSibling(final int offset) {
+  private void gotoSibling(int offset) {
     if (offset > 0 && changeInfo.currentRevision().equals(revision)) {
       return;
     }
@@ -598,24 +638,24 @@
     KeyCommandSet keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
       @Override
-      public void onKeyPress(final KeyPressEvent event) {
+      public void onKeyPress(KeyPressEvent event) {
         Gerrit.displayLastChangeList();
       }
     });
     keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadChange()) {
       @Override
-      public void onKeyPress(final KeyPressEvent event) {
+      public void onKeyPress(KeyPressEvent event) {
         Gerrit.display(PageLinks.toChange(changeId));
       }
     });
     keysNavigation.add(new KeyCommand(0, 'n', Util.C.keyNextPatchSet()) {
         @Override
-        public void onKeyPress(final KeyPressEvent event) {
+        public void onKeyPress(KeyPressEvent event) {
           gotoSibling(1);
         }
       }, new KeyCommand(0, 'p', Util.C.keyPreviousPatchSet()) {
         @Override
-        public void onKeyPress(final KeyPressEvent event) {
+        public void onKeyPress(KeyPressEvent event) {
           gotoSibling(-1);
         }
       });
@@ -858,44 +898,9 @@
     }
   }
 
-  private void loadConfigInfo(final ChangeInfo info, final String base) {
-    info.revisions().copyKeysIntoChildren("name");
-    if (edit != null) {
-      edit.setName(edit.commit().commit());
-      info.setEdit(edit);
-      if (edit.hasFiles()) {
-        edit.files().copyKeysIntoChildren("path");
-      }
-      info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
-      JsArray<RevisionInfo> list = info.revisions().values();
-
-      // Edit is converted to a regular revision (with number = 0) and
-      // added to the list of revisions. Additionally under certain
-      // circumstances change edit is assigned to be the current revision
-      // and is selected to be shown on the change screen.
-      // We have two different strategies to assign edit to the current ps:
-      // 1. revision == null: no revision is selected, so use the edit only
-      //    if it is based on the latest patch set
-      // 2. edit was selected explicitly from ps drop down:
-      //    use the edit regardless of which patch set it is based on
-      if (revision == null) {
-        RevisionInfo.sortRevisionInfoByNumber(list);
-        RevisionInfo rev = list.get(list.length() - 1);
-        if (rev.isEdit()) {
-          info.setCurrentRevision(rev.name());
-        }
-      } else if (revision.equals("edit") || revision.equals("0")) {
-        for (int i = 0; i < list.length(); i++) {
-          RevisionInfo r = list.get(i);
-          if (r.isEdit()) {
-            info.setCurrentRevision(r.name());
-            break;
-          }
-        }
-      }
-    }
-    final RevisionInfo rev = resolveRevisionToDisplay(info);
-    final RevisionInfo b = resolveRevisionOrPatchSetId(info, base, null);
+  private void loadConfigInfo(final ChangeInfo info, String base) {
+    RevisionInfo rev = info.revision(revision);
+    RevisionInfo b = resolveRevisionOrPatchSetId(info, base, null);
 
     CallbackGroup group = new CallbackGroup();
     Timestamp lastReply = myLastReply(info);
@@ -944,10 +949,10 @@
     return null;
   }
 
-  private void loadDiff(final RevisionInfo base, final RevisionInfo rev,
-      final Timestamp myLastReply, CallbackGroup group) {
-    final List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
-    final List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
+  private void loadDiff(RevisionInfo base, RevisionInfo rev,
+      Timestamp myLastReply, CallbackGroup group) {
+    List<NativeMap<JsArray<CommentInfo>>> comments = loadComments(rev, group);
+    List<NativeMap<JsArray<CommentInfo>>> drafts = loadDrafts(rev, group);
     loadFileList(base, rev, myLastReply, group, comments, drafts);
 
     if (Gerrit.isSignedIn() && fileTableMode == FileTable.Mode.REVIEW) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
index e73c70a..9afcd4f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
@@ -49,6 +49,6 @@
   }
 
   private static String url(PatchSet.Id ps, CommentInfo info) {
-    return Dispatcher.toSideBySide(null, ps, info.path());
+    return Dispatcher.toPatch(null, ps, info.path());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index 01337ea..71cc7fb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -284,7 +284,7 @@
     return info.binary()
       ? Dispatcher.toUnified(base, curr, info.path())
       : mode == Mode.REVIEW
-            ? Dispatcher.toSideBySide(base, curr, info.path())
+            ? Dispatcher.toPatch(base, curr, info.path())
             : Dispatcher.toEditScreen(curr, info.path());
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
index ad4debe..9873bed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
@@ -88,7 +88,7 @@
   }
 
   private static String url(PatchSet.Id ps, CommentInfo info) {
-    return Dispatcher.toSideBySide(null, ps, info.path(),
+    return Dispatcher.toPatch(null, ps, info.path(),
         info.side() == Side.PARENT ? DisplaySide.A : DisplaySide.B,
         info.line());
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 1fb997f..0a97729 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -33,18 +33,10 @@
   String outgoingReviews();
   String recentlyClosed();
 
-  String starredHeading();
-  String watchedHeading();
-  String draftsHeading();
-  String allOpenChanges();
-  String allAbandonedChanges();
-  String allMergedChanges();
-
   String changeTableColumnSubject();
   String changeTableColumnSize();
   String changeTableColumnStatus();
   String changeTableColumnOwner();
-  String changeTableColumnReviewers();
   String changeTableColumnProject();
   String changeTableColumnBranch();
   String changeTableColumnLastUpdate();
@@ -57,9 +49,6 @@
   String changeTablePagePrev();
   String changeTablePageNext();
   String upToChangeList();
-  String expandCollapseDependencies();
-  String previousPatchSet();
-  String nextPatchSet();
   String keyReloadChange();
   String keyNextPatchSet();
   String keyPreviousPatchSet();
@@ -76,11 +65,7 @@
   String patchTableColumnDiff();
   String patchTableDiffSideBySide();
   String patchTableDiffUnified();
-  String patchTableDownloadPreImage();
-  String patchTableDownloadPostImage();
-  String patchTableBinary();
   String commitMessage();
-  String fileCommentHeader();
 
   String patchTablePrev();
   String patchTableNext();
@@ -90,70 +75,22 @@
   String prevPatchLinkIcon();
   String nextPatchLinkIcon();
 
-  String changeScreenIncludedIn();
-  String changeScreenDependencies();
-  String changeScreenDependsOn();
-  String changeScreenNeededBy();
-  String changeScreenComments();
-  String changeScreenAddComment();
-
-  String approvalTableReviewer();
-  String approvalTableAddReviewer();
-  String approvalTableRemoveNotPermitted();
-  String approvalTableCouldNotRemove();
   String approvalTableAddReviewerHint();
   String approvalTableAddManyReviewersConfirmationDialogTitle();
 
-  String changeInfoBlockOwner();
-  String changeInfoBlockProject();
-  String changeInfoBlockBranch();
-  String changeInfoBlockTopic();
-  String changeInfoBlockTopicAlterTopicToolTip();
   String changeInfoBlockUploaded();
   String changeInfoBlockUpdated();
-  String changeInfoBlockStatus();
-  String changeInfoBlockSubmitType();
   String changePermalink();
-  String changeInfoBlockCanMerge();
-  String changeInfoBlockCanMergeYes();
-  String changeInfoBlockCanMergeNo();
-
-  String buttonAlterTopic();
-  String buttonAlterTopicBegin();
-  String buttonAlterTopicSend();
-  String buttonAlterTopicCancel();
-  String headingAlterTopicMessage();
-  String alterTopicTitle();
-  String alterTopicLabel();
-
-  String includedInTableBranch();
-  String includedInTableTag();
 
   String messageNoAuthor();
-  String messageExpandMostRecent();
-  String messageExpandRecent();
-  String messageExpandAll();
-  String messageCollapseAll();
-  String messageNeedsRebaseOrHasDependency();
 
   String sideBySide();
   String unifiedDiff();
 
-  String patchSetInfoAuthor();
-  String patchSetInfoCommitter();
-  String patchSetInfoDownload();
-  String patchSetInfoParents();
-  String patchSetWithDraftCommentsToolTip();
-  String initialCommit();
-
-  String buttonRebaseChange();
-
-  String buttonRevertChangeBegin();
   String buttonRevertChangeSend();
   String headingRevertMessage();
   String revertChangeTitle();
 
-  String buttonCherryPickChangeBegin();
   String buttonCherryPickChangeSend();
   String headingCherryPickBranch();
   String cherryPickCommitMessage();
@@ -165,38 +102,14 @@
   String rebasePlaceholderMessage();
   String rebaseTitle();
 
-  String buttonAbandonChangeBegin();
-  String buttonAbandonChangeSend();
-  String headingAbandonMessage();
-  String abandonChangeTitle();
-  String referenceVersion();
   String baseDiffItem();
   String autoMerge();
 
-  String buttonReview();
-  String buttonPublishCommentsSend();
-  String buttonPublishCommentsCancel();
-  String headingCoverMessage();
-  String headingPatchComments();
-
-  String buttonRestoreChangeBegin();
-  String restoreChangeTitle();
-  String headingRestoreMessage();
-  String buttonRestoreChangeSend();
-
-  String buttonPublishPatchSet();
-
-  String buttonDeleteDraftChange();
-  String buttonDeleteDraftPatchSet();
-
   String pagedChangeListPrev();
   String pagedChangeListNext();
 
-  String draftPatchSetLabel();
-
   String reviewed();
   String submitFailed();
-  String buttonClose();
 
   String diffAllSideBySide();
   String diffAllUnified();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index a5fa7b4..5a3ce66 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -8,23 +8,16 @@
 notCurrent = Not Current
 changeEdit = Change Edit
 
-starredHeading = Starred Changes
-watchedHeading = Open Changes of Watched Projects
-draftsHeading = Changes with unpublished drafts
 myDashboardTitle = My Reviews
 unknownDashboardTitle = Code Review Dashboard
 incomingReviews = Incoming reviews
 outgoingReviews = Outgoing reviews
 recentlyClosed = Recently closed
-allOpenChanges = All open changes
-allAbandonedChanges = All abandoned changes
-allMergedChanges = All merged changes
 
 changeTableColumnSubject = Subject
 changeTableColumnSize = Size
 changeTableColumnStatus = Status
 changeTableColumnOwner = Owner
-changeTableColumnReviewers = Reviewers
 changeTableColumnProject = Project
 changeTableColumnBranch = Branch
 changeTableColumnLastUpdate = Updated
@@ -37,9 +30,6 @@
 changeTablePagePrev = Previous page of changes
 changeTablePageNext = Next page of changes
 upToChangeList = Up to change list
-expandCollapseDependencies = Expands / Collapses dependencies section
-previousPatchSet = Previous patch set
-nextPatchSet = Next patch set
 keyReloadChange = Reload change
 keyNextPatchSet = Next patch set
 keyPreviousPatchSet = Previous patch set
@@ -57,89 +47,32 @@
 patchTableColumnDiff = Diff
 patchTableDiffSideBySide = Side-by-Side
 patchTableDiffUnified = Unified
-patchTableDownloadPreImage = old
-patchTableDownloadPostImage = new
-patchTableBinary = Binary
 commitMessage = Commit Message
-fileCommentHeader = File Comment:
 
 patchTablePrev = Previous file
 patchTableNext = Next file
 patchTableOpenDiff = Open diff
 patchTableOpenUnifiedDiff = Open unified diff
 
-changeScreenIncludedIn =  Included in
-changeScreenDependencies =  Dependencies
-changeScreenDependsOn = Depends On
-changeScreenNeededBy = Needed By
-changeScreenComments = Comments
-changeScreenAddComment = Add Comment
-
-approvalTableReviewer = Reviewer
-approvalTableAddReviewer = Add Reviewer
-approvalTableRemoveNotPermitted = Not allowed to remove reviewer
-approvalTableCouldNotRemove = Could not remove reviewer
 approvalTableAddReviewerHint = Name or Email or Group
 approvalTableAddManyReviewersConfirmationDialogTitle = Adding Group Members as Reviewers
 
-changeInfoBlockOwner = Owner
-changeInfoBlockProject = Project
-changeInfoBlockBranch = Branch
-changeInfoBlockTopic = Topic
-changeInfoBlockTopicAlterTopicToolTip = Edit Topic
 changeInfoBlockUploaded = Uploaded
 changeInfoBlockUpdated = Updated
-changeInfoBlockStatus = Status
-changeInfoBlockSubmitType = Submit Type
 changePermalink = Permalink
-changeInfoBlockCanMerge = Can Merge
-changeInfoBlockCanMergeYes = Yes
-changeInfoBlockCanMergeNo = No
-
-buttonAlterTopic = Edit Topic
-buttonAlterTopicBegin = Edit Topic
-buttonAlterTopicSend = Update Topic
-buttonAlterTopicCancel = Cancel
-headingAlterTopicMessage = Edit Topic Message:
-alterTopicTitle = Code Review - Edit Topic
-alterTopicLabel = New Topic Name:
-
-includedInTableBranch = Branch Name
-includedInTableTag = Tag Name
 
 messageNoAuthor = Gerrit Code Review
-messageExpandMostRecent = Expand Most Recent
-messageExpandRecent = Expand Recent
-messageExpandAll = Expand All
-messageCollapseAll = Collapse All
-messageNeedsRebaseOrHasDependency = Need Rebase or Has Dependency
 
 sideBySide = Side by Side
 unifiedDiff = Unified Diff
 
-patchSetInfoAuthor = Author
-patchSetInfoCommitter = Committer
-patchSetInfoDownload = Download
-patchSetInfoParents = Parent(s)
-patchSetWithDraftCommentsToolTip = Draft comment(s) inside
-initialCommit = Initial Commit
-
-buttonAbandonChangeBegin = Abandon Change
-buttonAbandonChangeSend = Abandon Change
-headingAbandonMessage = Abandon Message:
-abandonChangeTitle = Code Review - Abandon Change
-referenceVersion = Reference Version:
 baseDiffItem = Base
 autoMerge = Auto Merge
 
-buttonRebaseChange = Rebase Change
-
-buttonRevertChangeBegin = Revert Change
 buttonRevertChangeSend = Revert Change
 headingRevertMessage = Revert Commit Message:
 revertChangeTitle = Code Review - Revert Merged Change
 
-buttonCherryPickChangeBegin = Cherry Pick To
 buttonCherryPickChangeSend = Cherry Pick Change
 headingCherryPickBranch = Cherry Pick to Branch:
 cherryPickCommitMessage = Cherry Pick Commit Message:
@@ -151,34 +84,15 @@
 rebasePlaceholderMessage = (subject, change number, or leave empty)
 rebaseTitle = Code Review - Rebase Change
 
-buttonRestoreChangeBegin = Restore Change
-restoreChangeTitle = Code Review - Restore Change
-headingRestoreMessage = Restore Message:
-buttonRestoreChangeSend = Restore Change
-
-buttonReview = Review
-buttonPublishCommentsSend = Publish Comments
-buttonPublishCommentsCancel = Cancel
-headingCoverMessage = Cover Message:
-headingPatchComments = Patch Comments:
-
-buttonPublishPatchSet = Publish
-
-buttonDeleteDraftChange = Delete Draft Change
-buttonDeleteDraftPatchSet = Delete Draft Patch Set
-
 pagedChangeListPrev = &#x21e6;Prev
 pagedChangeListNext = Next&#x21e8;
 
-draftPatchSetLabel = (DRAFT)
-
 upToChangeIconLink = &#x21e7;Up to change
 prevPatchLinkIcon = &#x21e6;
 nextPatchLinkIcon = &#x21e8;
 
 reviewed = Reviewed
 submitFailed = Submit Failed
-buttonClose = Close
 
 diffAllSideBySide = All Side-by-Side
 diffAllUnified = All Unified
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
index 4649d81..cf0f9aa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
@@ -19,7 +19,6 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.JumpKeys;
 import com.google.gerrit.client.account.DiffPreferences;
 import com.google.gerrit.client.change.ChangeScreen;
 import com.google.gerrit.client.change.FileTable;
@@ -249,7 +248,6 @@
     super.onShowView();
 
     Window.enableScrolling(false);
-    JumpKeys.enable(false);
     if (prefs.hideTopMenu()) {
       Gerrit.setHeaderVisible(false);
     }
@@ -298,7 +296,6 @@
 
     Window.enableScrolling(true);
     Gerrit.setHeaderVisible(true);
-    JumpKeys.enable(true);
   }
 
   private void removeKeyHandlerRegistrations() {
@@ -320,7 +317,7 @@
         .on("O", getCommentManager().toggleOpenBox(cm))
         .on("Enter", getCommentManager().toggleOpenBox(cm))
         .on("N", maybeNextVimSearch(cm))
-        .on("E", openEditScreen(cm))
+        .on("Ctrl-Alt-E", openEditScreen(cm))
         .on("P", getChunkManager().diffChunkNav(cm, Direction.PREV))
         .on("Shift-M", header.reviewedAndNext())
         .on("Shift-N", maybePrevVimSearch(cm))
@@ -376,12 +373,65 @@
           public void run() {
             cm.execCommand("selectAll");
           }
+        })
+        .on("G O", new Runnable() {
+          @Override
+          public void run() {
+            Gerrit.display(PageLinks.toChangeQuery("status:open"));
+          }
+        })
+        .on("G M", new Runnable() {
+          @Override
+          public void run() {
+            Gerrit.display(PageLinks.toChangeQuery("status:merged"));
+          }
+        })
+        .on("G A", new Runnable() {
+          @Override
+          public void run() {
+            Gerrit.display(PageLinks.toChangeQuery("status:abandoned"));
+          }
         });
+        if (Gerrit.isSignedIn()) {
+          keyMap.on("G I", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(PageLinks.MINE);
+            }
+          })
+          .on("G D", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(PageLinks.toChangeQuery("owner:self is:draft"));
+            }
+          })
+          .on("G C", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(PageLinks.toChangeQuery("has:draft"));
+            }
+          })
+          .on("G W", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(
+                  PageLinks.toChangeQuery("is:watched status:open"));
+            }
+          })
+          .on("G S", new Runnable() {
+            @Override
+            public void run() {
+              Gerrit.display(PageLinks.toChangeQuery("is:starred"));
+            }
+          });
+        }
+
     if (revision.get() != 0) {
       cm.on("beforeSelectionChange", onSelectionChange(cm));
       cm.on("gutterClick", onGutterClick(cm));
       keyMap.on("C", getCommentManager().newDraftCallback(cm));
     }
+    CodeMirror.normalizeKeyMap(keyMap); // Needed to for multi-stroke keymaps
     cm.addKeyMap(keyMap);
   }
 
@@ -448,6 +498,8 @@
           header.toggleReviewed().run();
         }
       });
+      keysAction.add(new NoOpKeyCommand(KeyCommand.M_CTRL | KeyCommand.M_ALT,
+          'e', Gerrit.C.keyEditor()));
     }
     keysAction.add(new KeyCommand(
         KeyCommand.M_SHIFT, 'm', PatchUtil.C.markAsReviewedAndGoToNext()) {
@@ -628,14 +680,12 @@
   }
 
   void resizeCodeMirror() {
-    int height = getCodeMirrorHeight();
+    int height = header.getOffsetHeight() + getDiffTable().getHeaderHeight();
     for (CodeMirror cm : getCms()) {
       cm.adjustHeight(height);
     }
   }
 
-  abstract int getCodeMirrorHeight();
-
   abstract ChunkManager getChunkManager();
 
   abstract CommentManager getCommentManager();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index 47af842..58cc2c0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -91,13 +91,7 @@
     }
   }
 
-  int getHeaderHeight() {
-    int h = patchSetSelectBoxA.getOffsetHeight();
-    if (header) {
-      h += diffHeaderRow.getOffsetHeight();
-    }
-    return h;
-  }
+  abstract int getHeaderHeight();
 
   ChangeType getChangeType() {
     return changeType;
@@ -153,4 +147,8 @@
   }
 
   abstract DiffScreen getDiffScreen();
+
+  boolean hasHeader() {
+    return header;
+  }
 }
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 ed174d2..931636f 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
@@ -201,7 +201,7 @@
     operation(new Runnable() {
       @Override
       public void run() {
-        // Estimate initial CM3 height, fixed up in onShowView.
+        // Estimate initial CodeMirror height, fixed up in onShowView.
         int height = Window.getClientHeight()
             - (Gerrit.getHeaderFooterHeight() + 18);
         cmA.setHeight(height);
@@ -392,11 +392,6 @@
   }
 
   @Override
-  int getCodeMirrorHeight() {
-    return header.getOffsetHeight() + diffTable.getHeaderHeight();
-  }
-
-  @Override
   CodeMirror[] getCms() {
     return new CodeMirror[]{cmA, cmB};
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
index 7b9be7a..88e431f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java
@@ -156,23 +156,7 @@
 
   @Override
   void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line) {
-    if (cm.somethingSelected()) {
-      FromTo fromTo = cm.getSelectedRange();
-      Pos end = fromTo.to();
-      if (end.ch() == 0) {
-        end.line(end.line() - 1);
-        end.ch(cm.getLine(end.line()).length());
-      }
-
-      addDraftBox(cm.side(), CommentInfo.create(
-              getPath(),
-              getStoredSideFromDisplaySide(cm.side()),
-              line,
-              CommentRange.create(fromTo))).setEdit(true);
-      cm.setSelection(cm.getCursor());
-    } else {
-      insertNewDraft(cm.side(), line);
-    }
+    insertNewDraft(cm.side(), line);
   }
 
   /**
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
index 79865ff..a286356 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
@@ -102,4 +102,13 @@
   SideBySide getDiffScreen() {
     return parent;
   }
+
+  @Override
+  int getHeaderHeight() {
+    int h = patchSetSelectBoxA.getOffsetHeight();
+    if (hasHeader()) {
+      h += diffHeaderRow.getOffsetHeight();
+    }
+    return h;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
index 21b7a4c..904d47d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
@@ -192,7 +192,7 @@
     operation(new Runnable() {
       @Override
       public void run() {
-        // Estimate initial CM3 height, fixed up in onShowView.
+        // Estimate initial CodeMirror height, fixed up in onShowView.
         int height = Window.getClientHeight()
             - (Gerrit.getHeaderFooterHeight() + 18);
         cm.setHeight(height);
@@ -371,12 +371,6 @@
   }
 
   @Override
-  void resizeCodeMirror() {
-    int hdr = header.getOffsetHeight() + diffTable.getHeaderHeight();
-    cm.adjustHeight(hdr);
-  }
-
-  @Override
   void operation(final Runnable apply) {
     cm.operation(new Runnable() {
       @Override
@@ -387,14 +381,6 @@
   }
 
   @Override
-  int getCodeMirrorHeight() {
-    int rest =
-        Gerrit.getHeaderFooterHeight() + header.getOffsetHeight()
-            + diffTable.getHeaderHeight() + 5; // Estimate
-    return Window.getClientHeight() - rest;
-  }
-
-  @Override
   CodeMirror[] getCms() {
     return new CodeMirror[] {cm};
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
index 832635c..866f0f7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
@@ -292,8 +292,17 @@
       res = -res - 1;
       if (res > 0) {
         UnifiedDiffChunkInfo info = chunks.get(res - 1);
-        return new LineSidePair(
-            info.getStart() + cmLine - info.getCmLine(), info.getSide());
+        int lineOnInfoSide = info.getStart() + cmLine - info.getCmLine();
+        if (lineOnInfoSide > info.getEnd()
+            && info.getSide() == DisplaySide.A) {
+          // For the common region after a deletion chunk, return the line and
+          // side info on side B
+          return new LineSidePair(
+              getLineMapper().lineOnOther(DisplaySide.A, lineOnInfoSide)
+                  .getLine(), DisplaySide.B);
+        } else {
+          return new LineSidePair(lineOnInfoSide, info.getSide());
+        }
       } else {
         // Always return side B
         return new LineSidePair(cmLine, DisplaySide.B);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
index 059101c..9038fb4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java
@@ -26,7 +26,6 @@
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.LineHandle;
 import net.codemirror.lib.Pos;
-import net.codemirror.lib.TextMarker.FromTo;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -155,29 +154,7 @@
     DisplaySide side = gutterClass.equals(UnifiedTable.style.lineNumbersLeft())
         ? DisplaySide.A
         : DisplaySide.B;
-    if (cm.somethingSelected()) {
-      FromTo fromTo = cm.getSelectedRange();
-      Pos end = fromTo.to();
-      if (end.ch() == 0) {
-        end.line(end.line() - 1);
-        end.ch(cm.getLine(end.line()).length());
-      }
-
-      LineSidePair pair = host.getLineSidePairFromCmLine(cmLinePlusOne - 1);
-      int line = pair.getLine();
-      if (pair.getSide() != side) {
-        line = host.lineOnOther(pair.getSide(), line).getLine();
-      }
-
-      addDraftBox(side, CommentInfo.create(
-              getPath(),
-              getStoredSideFromDisplaySide(side),
-              line + 1,
-              CommentRange.create(fromTo))).setEdit(true);
-      cm.setSelection(cm.getCursor());
-    } else {
-      insertNewDraft(side, cmLinePlusOne);
-    }
+    insertNewDraft(side, cmLinePlusOne);
   }
 
   /**
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
index 5d33a20..c936c7a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java
@@ -65,4 +65,14 @@
   Unified getDiffScreen() {
     return parent;
   }
+
+  @Override
+  int getHeaderHeight() {
+    int h = patchSetSelectBoxA.getOffsetHeight()
+        + patchSetSelectBoxB.getOffsetHeight();
+    if (hasHeader()) {
+      h += diffHeaderRow.getOffsetHeight();
+    }
+    return h;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index a546c62..5799433 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -71,6 +71,7 @@
 
 import net.codemirror.lib.CodeMirror;
 import net.codemirror.lib.CodeMirror.ChangesHandler;
+import net.codemirror.lib.CodeMirror.CommandRunner;
 import net.codemirror.lib.Configuration;
 import net.codemirror.lib.KeyMap;
 import net.codemirror.lib.Pos;
@@ -435,6 +436,13 @@
         .set("keyMap", prefs.keyMapType().name().toLowerCase())
         .set("theme", prefs.theme().name().toLowerCase())
         .set("mode", mode != null ? mode.mode() : null));
+
+    CodeMirror.addCommand("save", new CommandRunner() {
+      @Override
+      public void run(CodeMirror instance) {
+        save().run();
+      }
+    });
   }
 
   private void renderLinks(EditFileInfo editInfo,
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index 9bc9ad4..ee4c050 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -364,6 +364,17 @@
     $wnd.CodeMirror.keyMap[name] = km
   }-*/;
 
+  public static final native void normalizeKeyMap(KeyMap km) /*-{
+    $wnd.CodeMirror.normalizeKeyMap(km);
+  }-*/;
+
+  public static final native void addCommand(String name, CommandRunner runner) /*-{
+    $wnd.CodeMirror.commands[name] = function(cm) {
+      runner.@net.codemirror.lib.CodeMirror.CommandRunner::run(
+        Lnet/codemirror/lib/CodeMirror;)(cm);
+    };
+  }-*/;
+
   public final native Vim vim() /*-{
     return this;
   }-*/;
@@ -428,4 +439,8 @@
   public interface ChangesHandler {
     void handle(CodeMirror instance);
   }
+
+  public interface CommandRunner {
+    void run(CodeMirror instance);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
index c439e10..9c82c06 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -37,49 +37,119 @@
 
   static {
     indexModes(new DataResource[] {
+      Modes.I.apl(),
+      Modes.I.asciiarmor(),
+      Modes.I.asn_1(),
+      Modes.I.asterisk(),
+      Modes.I.brainfuck(),
       Modes.I.clike(),
       Modes.I.clojure(),
+      Modes.I.cmake(),
+      Modes.I.cobol(),
       Modes.I.coffeescript(),
       Modes.I.commonlisp(),
+      Modes.I.crystal(),
       Modes.I.css(),
+      Modes.I.cypher(),
       Modes.I.d(),
       Modes.I.dart(),
       Modes.I.diff(),
+      Modes.I.django(),
       Modes.I.dockerfile(),
       Modes.I.dtd(),
+      Modes.I.dylan(),
+      Modes.I.ebnf(),
+      Modes.I.ecl(),
+      Modes.I.eiffel(),
+      Modes.I.elm(),
       Modes.I.erlang(),
+      Modes.I.factor(),
+      Modes.I.fcl(),
+      Modes.I.forth(),
+      Modes.I.fortran(),
       Modes.I.gas(),
       Modes.I.gerrit_commit(),
       Modes.I.gfm(),
+      Modes.I.gherkin(),
       Modes.I.go(),
       Modes.I.groovy(),
+      Modes.I.haml(),
+      Modes.I.handlebars(),
+      Modes.I.haskell_literate(),
       Modes.I.haskell(),
+      Modes.I.haxe(),
+      Modes.I.htmlembedded(),
       Modes.I.htmlmixed(),
+      Modes.I.http(),
+      Modes.I.idl(),
+      Modes.I.jade(),
       Modes.I.javascript(),
+      Modes.I.jinja2(),
+      Modes.I.jsx(),
+      Modes.I.julia(),
+      Modes.I.livescript(),
       Modes.I.lua(),
       Modes.I.markdown(),
+      Modes.I.mathematica(),
+      Modes.I.mirc(),
+      Modes.I.mllike(),
+      Modes.I.modelica(),
+      Modes.I.mscgen(),
+      Modes.I.mumps(),
+      Modes.I.nginx(),
+      Modes.I.nsis(),
+      Modes.I.ntriples(),
+      Modes.I.octave(),
+      Modes.I.oz(),
+      Modes.I.pascal(),
+      Modes.I.pegjs(),
       Modes.I.perl(),
       Modes.I.php(),
       Modes.I.pig(),
       Modes.I.properties(),
+      Modes.I.protobuf(),
       Modes.I.puppet(),
       Modes.I.python(),
+      Modes.I.q(),
       Modes.I.r(),
+      Modes.I.rpm(),
       Modes.I.rst(),
       Modes.I.ruby(),
+      Modes.I.rust(),
       Modes.I.scheme(),
       Modes.I.shell(),
       Modes.I.smalltalk(),
+      Modes.I.smarty(),
+      Modes.I.solr(),
       Modes.I.soy(),
+      Modes.I.sparql(),
+      Modes.I.spreadsheet(),
       Modes.I.sql(),
       Modes.I.stex(),
+      Modes.I.stylus(),
       Modes.I.swift(),
       Modes.I.tcl(),
+      Modes.I.textile(),
+      Modes.I.tiddlywiki(),
+      Modes.I.tiki(),
+      Modes.I.toml(),
+      Modes.I.tornado(),
+      Modes.I.troff(),
+      Modes.I.ttcn_cfg(),
+      Modes.I.ttcn(),
+      Modes.I.turtle(),
+      Modes.I.twig(),
+      Modes.I.vb(),
+      Modes.I.vbscript(),
       Modes.I.velocity(),
       Modes.I.verilog(),
       Modes.I.vhdl(),
+      Modes.I.vue(),
       Modes.I.xml(),
+      Modes.I.xquery(),
+      Modes.I.yaml_frontmatter(),
       Modes.I.yaml(),
+      Modes.I.z80(),
     });
 
     alias("application/x-httpd-php-open", "application/x-httpd-php");
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
index 4faaccf..593dc29 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -22,49 +22,122 @@
 public interface Modes extends ClientBundle {
   public static final Modes I = GWT.create(Modes.class);
 
+  @Source("apl.js") @DoNotEmbed DataResource apl();
+  @Source("asciiarmor.js") @DoNotEmbed DataResource asciiarmor();
+  @Source("asn.1.js") @DoNotEmbed DataResource asn_1();
+  @Source("asterisk.js") @DoNotEmbed DataResource asterisk();
+  @Source("brainfuck.js") @DoNotEmbed DataResource brainfuck();
   @Source("clike.js") @DoNotEmbed DataResource clike();
   @Source("clojure.js") @DoNotEmbed DataResource clojure();
+  @Source("cmake.js") @DoNotEmbed DataResource cmake();
+  @Source("cobol.js") @DoNotEmbed DataResource cobol();
   @Source("coffeescript.js") @DoNotEmbed DataResource coffeescript();
   @Source("commonlisp.js") @DoNotEmbed DataResource commonlisp();
+  @Source("crystal.js") @DoNotEmbed DataResource crystal();
   @Source("css.js") @DoNotEmbed DataResource css();
+  @Source("cypher.js") @DoNotEmbed DataResource cypher();
   @Source("d.js") @DoNotEmbed DataResource d();
   @Source("dart.js") @DoNotEmbed DataResource dart();
   @Source("diff.js") @DoNotEmbed DataResource diff();
+  @Source("django.js") @DoNotEmbed DataResource django();
   @Source("dockerfile.js") @DoNotEmbed DataResource dockerfile();
   @Source("dtd.js") @DoNotEmbed DataResource dtd();
+  @Source("dylan.js") @DoNotEmbed DataResource dylan();
+  @Source("ebnf.js") @DoNotEmbed DataResource ebnf();
+  @Source("ecl.js") @DoNotEmbed DataResource ecl();
+  @Source("eiffel.js") @DoNotEmbed DataResource eiffel();
+  @Source("elm.js") @DoNotEmbed DataResource elm();
   @Source("erlang.js") @DoNotEmbed DataResource erlang();
+  @Source("factor.js") @DoNotEmbed DataResource factor();
+  @Source("fcl.js") @DoNotEmbed DataResource fcl();
+  @Source("forth.js") @DoNotEmbed DataResource forth();
+  @Source("fortran.js") @DoNotEmbed DataResource fortran();
   @Source("gas.js") @DoNotEmbed DataResource gas();
   @Source("gerrit/commit.js") @DoNotEmbed DataResource gerrit_commit();
   @Source("gfm.js") @DoNotEmbed DataResource gfm();
+  @Source("gherkin.js") @DoNotEmbed DataResource gherkin();
   @Source("go.js") @DoNotEmbed DataResource go();
   @Source("groovy.js") @DoNotEmbed DataResource groovy();
+  @Source("haml.js") @DoNotEmbed DataResource haml();
+  @Source("handlebars.js") @DoNotEmbed DataResource handlebars();
+  @Source("haskell-literate.js") @DoNotEmbed DataResource haskell_literate();
   @Source("haskell.js") @DoNotEmbed DataResource haskell();
+  @Source("haxe.js") @DoNotEmbed DataResource haxe();
+  @Source("htmlembedded.js") @DoNotEmbed DataResource htmlembedded();
   @Source("htmlmixed.js") @DoNotEmbed DataResource htmlmixed();
+  @Source("http.js") @DoNotEmbed DataResource http();
+  @Source("idl.js") @DoNotEmbed DataResource idl();
+  @Source("jade.js") @DoNotEmbed DataResource jade();
   @Source("javascript.js") @DoNotEmbed DataResource javascript();
+  @Source("jinja2.js") @DoNotEmbed DataResource jinja2();
+  @Source("jsx.js") @DoNotEmbed DataResource jsx();
+  @Source("julia.js") @DoNotEmbed DataResource julia();
+  @Source("livescript.js") @DoNotEmbed DataResource livescript();
   @Source("lua.js") @DoNotEmbed DataResource lua();
   @Source("markdown.js") @DoNotEmbed DataResource markdown();
+  @Source("mathematica.js") @DoNotEmbed DataResource mathematica();
+  @Source("mirc.js") @DoNotEmbed DataResource mirc();
+  @Source("mllike.js") @DoNotEmbed DataResource mllike();
+  @Source("modelica.js") @DoNotEmbed DataResource modelica();
+  @Source("mscgen.js") @DoNotEmbed DataResource mscgen();
+  @Source("mumps.js") @DoNotEmbed DataResource mumps();
+  @Source("nginx.js") @DoNotEmbed DataResource nginx();
+  @Source("nsis.js") @DoNotEmbed DataResource nsis();
+  @Source("ntriples.js") @DoNotEmbed DataResource ntriples();
+  @Source("octave.js") @DoNotEmbed DataResource octave();
+  @Source("oz.js") @DoNotEmbed DataResource oz();
+  @Source("pascal.js") @DoNotEmbed DataResource pascal();
+  @Source("pegjs.js") @DoNotEmbed DataResource pegjs();
   @Source("perl.js") @DoNotEmbed DataResource perl();
   @Source("php.js") @DoNotEmbed DataResource php();
   @Source("pig.js") @DoNotEmbed DataResource pig();
   @Source("properties.js") @DoNotEmbed DataResource properties();
+  @Source("protobuf.js") @DoNotEmbed DataResource protobuf();
   @Source("puppet.js") @DoNotEmbed DataResource puppet();
   @Source("python.js") @DoNotEmbed DataResource python();
+  @Source("q.js") @DoNotEmbed DataResource q();
   @Source("r.js") @DoNotEmbed DataResource r();
+  @Source("rpm.js") @DoNotEmbed DataResource rpm();
   @Source("rst.js") @DoNotEmbed DataResource rst();
   @Source("ruby.js") @DoNotEmbed DataResource ruby();
+  @Source("rust.js") @DoNotEmbed DataResource rust();
+  @Source("sass.js") @DoNotEmbed DataResource sass();
   @Source("scheme.js") @DoNotEmbed DataResource scheme();
   @Source("shell.js") @DoNotEmbed DataResource shell();
+  @Source("sieve.js") @DoNotEmbed DataResource sieve();
+  @Source("slim.js") @DoNotEmbed DataResource slim();
   @Source("smalltalk.js") @DoNotEmbed DataResource smalltalk();
-  @Source("swift.js") @DoNotEmbed DataResource swift();
+  @Source("smarty.js") @DoNotEmbed DataResource smarty();
+  @Source("solr.js") @DoNotEmbed DataResource solr();
   @Source("soy.js") @DoNotEmbed DataResource soy();
+  @Source("sparql.js") @DoNotEmbed DataResource sparql();
+  @Source("spreadsheet.js") @DoNotEmbed DataResource spreadsheet();
   @Source("sql.js") @DoNotEmbed DataResource sql();
   @Source("stex.js") @DoNotEmbed DataResource stex();
+  @Source("stylus.js") @DoNotEmbed DataResource stylus();
+  @Source("swift.js") @DoNotEmbed DataResource swift();
   @Source("tcl.js") @DoNotEmbed DataResource tcl();
+  @Source("textile.js") @DoNotEmbed DataResource textile();
+  @Source("tiddlywiki.js") @DoNotEmbed DataResource tiddlywiki();
+  @Source("tiki.js") @DoNotEmbed DataResource tiki();
+  @Source("toml.js") @DoNotEmbed DataResource toml();
+  @Source("tornado.js") @DoNotEmbed DataResource tornado();
+  @Source("troff.js") @DoNotEmbed DataResource troff();
+  @Source("ttcn-cfg.js") @DoNotEmbed DataResource ttcn_cfg();
+  @Source("ttcn.js") @DoNotEmbed DataResource ttcn();
+  @Source("turtle.js") @DoNotEmbed DataResource turtle();
+  @Source("twig.js") @DoNotEmbed DataResource twig();
+  @Source("vb.js") @DoNotEmbed DataResource vb();
+  @Source("vbscript.js") @DoNotEmbed DataResource vbscript();
   @Source("velocity.js") @DoNotEmbed DataResource velocity();
   @Source("verilog.js") @DoNotEmbed DataResource verilog();
   @Source("vhdl.js") @DoNotEmbed DataResource vhdl();
+  @Source("vue.js") @DoNotEmbed DataResource vue();
   @Source("xml.js") @DoNotEmbed DataResource xml();
+  @Source("xquery.js") @DoNotEmbed DataResource xquery();
+  @Source("yaml-frontmatter.js") @DoNotEmbed DataResource yaml_frontmatter();
   @Source("yaml.js") @DoNotEmbed DataResource yaml();
+  @Source("z80.js") @DoNotEmbed DataResource z80();
 
   // When adding a resource, update static initializer in ModeInfo.
 }
diff --git a/gerrit-gwtui/src/test/resources/META-INF/gwt-test-utils.properties b/gerrit-gwtui/src/test/resources/META-INF/gwt-test-utils.properties
deleted file mode 100644
index c0cbb30..0000000
--- a/gerrit-gwtui/src/test/resources/META-INF/gwt-test-utils.properties
+++ /dev/null
@@ -1 +0,0 @@
-com.google.gerrit.GerritGwtUI = gwt-module
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/FontsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/FontsServlet.java
new file mode 100644
index 0000000..3a8c8cb
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/FontsServlet.java
@@ -0,0 +1,61 @@
+// 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.httpd.raw;
+
+import com.google.common.cache.Cache;
+import com.google.gerrit.launcher.GerritLauncher;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+class FontsServlet extends ResourceServlet {
+  private static final long serialVersionUID = 1L;
+
+  private final Path zip;
+  private final Path fonts;
+
+  FontsServlet(Cache<Path, Resource> cache, Path buckOut)
+      throws IOException {
+    super(cache, true);
+    zip = getZipPath(buckOut);
+    if (zip == null || !Files.exists(zip)) {
+      fonts = null;
+    } else {
+      fonts = GerritLauncher
+          .newZipFileSystem(zip)
+          .getPath("/");
+    }
+  }
+
+  @Override
+  protected Path getResourcePath(String pathInfo) throws IOException {
+    if (fonts == null) {
+      throw new IOException("No fonts found: " + zip
+          + ". Run `buck build //polygerrit-ui:fonts`?");
+    }
+    return fonts.resolve(pathInfo);
+  }
+
+  private static Path getZipPath(Path buckOut) {
+    if (buckOut == null) {
+      return null;
+    }
+    return buckOut.resolve("gen")
+        .resolve("polygerrit-ui")
+        .resolve("fonts")
+        .resolve("fonts.zip");
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
index 4256a9f..26ac8e1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -221,10 +221,11 @@
       Path buckOut = getPaths().buckOut;
       if (buckOut != null) {
         serve("/bower_components/*").with(BowerComponentsServlet.class);
+        serve("/fonts/*").with(FontsServlet.class);
       } else {
-        // In the war case, bower_components are either inlined by vulcanize, or
-        // live under /polygerrit_ui in the war file, so we don't need a
-        // separate servlet.
+        // In the war case, bower_components and fonts are either inlined
+        // by vulcanize, or live under /polygerrit_ui in the war file,
+        // so we don't need a separate servlet.
       }
 
       Key<HttpServlet> indexKey = named(POLYGERRIT_INDEX_SERVLET);
@@ -259,6 +260,13 @@
       return new BowerComponentsServlet(cache, getPaths().buckOut);
     }
 
+    @Provides
+    @Singleton
+    FontsServlet getFontsServlet(
+        @Named(CACHE) Cache<Path, Resource> cache) throws IOException {
+      return new FontsServlet(cache, getPaths().buckOut);
+    }
+
     private Path polyGerritBasePath() {
       Paths p = getPaths();
       if (options.forcePolyGerritDev()) {
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index f406f97..73eeb33 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -74,7 +74,6 @@
       System.err.println();
       System.err.println("The most commonly used commands are:");
       System.err.println("  init            Initialize a Gerrit installation");
-      System.err.println("  rebuild-notedb  Rebuild the review notes database");
       System.err.println("  reindex         Rebuild the secondary index");
       System.err.println("  daemon          Run the Gerrit network daemons");
       System.err.println("  gsql            Run the interactive query console");
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 8b508e2..8f7e899 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -358,7 +358,7 @@
 
     if (!schema.hasField(PROJECT)) {
       // Schema is not new enough to have project field. Ensure we have ID
-      // field, and call createOnlyWhenNotedbDisabled from toChangeData below.
+      // field, and call createOnlyWhenNoteDbDisabled from toChangeData below.
       if (fs.contains(LEGACY_ID.getName())) {
         return fs;
       } else {
@@ -389,9 +389,9 @@
           new Change.Id(doc.getField(idFieldName).numericValue().intValue());
       IndexableField project = doc.getField(PROJECT.getName());
       if (project == null) {
-        // Old schema without project field: we can safely assume notedb is
+        // Old schema without project field: we can safely assume NoteDb is
         // disabled.
-        cd = changeDataFactory.createOnlyWhenNotedbDisabled(db.get(), id);
+        cd = changeDataFactory.createOnlyWhenNoteDbDisabled(db.get(), id);
       } else {
         cd = changeDataFactory.create(
             db.get(), new Project.NameKey(project.stringValue()), id);
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
index 2c46fd3..9dd4399 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -20,14 +20,15 @@
 import com.google.common.collect.Maps;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.index.Index;
-import com.google.gerrit.server.index.IndexCollection;
-import com.google.gerrit.server.index.IndexDefinition;
-import com.google.gerrit.server.index.IndexDefinition.IndexFactory;
 import com.google.gerrit.server.index.OnlineReindexer;
 import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
+import com.google.gerrit.server.index.change.ChangeIndexDefinition;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
@@ -45,7 +46,6 @@
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.List;
-import java.util.Map;
 import java.util.TreeMap;
 
 @Singleton
@@ -55,13 +55,13 @@
 
   static final String CHANGES_PREFIX = "changes_";
 
-  private static class Version<V> {
-    private final Schema<V> schema;
+  private static class Version {
+    private final Schema<ChangeData> schema;
     private final int version;
     private final boolean exists;
     private final boolean ready;
 
-    private Version(Schema<V> schema, int version, boolean exists,
+    private Version(Schema<ChangeData> schema, int version, boolean exists,
         boolean ready) {
       checkArgument(schema == null || schema.getVersion() == version);
       this.schema = schema;
@@ -94,32 +94,33 @@
   }
 
   private final SitePaths sitePaths;
-  private final Map<String, IndexDefinition<?, ?, ?>> defs;
-  private final Map<String, OnlineReindexer<?, ?, ?>> reindexers;
+  private final LuceneChangeIndex.Factory indexFactory;
+  private final ChangeIndexCollection indexes;
+  private final ChangeIndexDefinition changeDef;
   private final boolean onlineUpgrade;
-  private final String runReindexMsg;
+  private OnlineReindexer<Change.Id, ChangeData, ChangeIndex> reindexer;
 
   @Inject
   LuceneVersionManager(
       @GerritServerConfig Config cfg,
       SitePaths sitePaths,
-      Collection<IndexDefinition<?, ?, ?>> defs) {
+      LuceneChangeIndex.Factory indexFactory,
+      ChangeIndexCollection indexes,
+      ChangeIndexDefinition changeDef) {
     this.sitePaths = sitePaths;
-    this.defs = Maps.newHashMapWithExpectedSize(defs.size());
-    for (IndexDefinition<?, ?, ?> def : defs) {
-      this.defs.put(def.getName(), def);
-    }
-
-    reindexers = Maps.newHashMapWithExpectedSize(defs.size());
-    onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
-    runReindexMsg =
-        "No index versions ready; run java -jar " +
-        sitePaths.gerrit_war.toAbsolutePath() +
-        " reindex";
+    this.indexFactory = indexFactory;
+    this.indexes = indexes;
+    this.changeDef = changeDef;
+    this.onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true);
   }
 
   @Override
   public void start() {
+    String runReindex =
+      "No index versions ready; run java -jar " +
+      sitePaths.gerrit_war.toAbsolutePath() +
+      " reindex";
+
     FileBasedConfig cfg;
     try {
       cfg = loadGerritIndexConfig(sitePaths);
@@ -128,25 +129,18 @@
     }
 
     if (!Files.exists(sitePaths.index_dir)) {
-      throw new ProvisionException(runReindexMsg);
+      throw new ProvisionException(runReindex);
     } else if (!Files.exists(sitePaths.index_dir)) {
       log.warn("Not a directory: %s", sitePaths.index_dir.toAbsolutePath());
-      throw new ProvisionException(runReindexMsg);
+      throw new ProvisionException(runReindex);
     }
 
-    for (IndexDefinition<?, ?, ?> def : defs.values()) {
-      initIndex(def, cfg);
-    }
-  }
-
-  private <K, V, I extends Index<K, V>> void initIndex(
-      IndexDefinition<K, V, I> def, FileBasedConfig cfg) {
-    TreeMap<Integer, Version<V>> versions = scanVersions(def, cfg);
+    TreeMap<Integer, Version> versions = scanVersions(cfg);
     // Search from the most recent ready version.
     // Write to the most recent ready version and the most recent version.
-    Version<V> search = null;
-    List<Version<V>> write = Lists.newArrayListWithCapacity(2);
-    for (Version<V> v : versions.descendingMap().values()) {
+    Version search = null;
+    List<Version> write = Lists.newArrayListWithCapacity(2);
+    for (Version v : versions.descendingMap().values()) {
       if (v.schema == null) {
         continue;
       }
@@ -162,35 +156,27 @@
       }
     }
     if (search == null) {
-      throw new ProvisionException(runReindexMsg);
+      throw new ProvisionException(runReindex);
     }
 
-    IndexFactory<K, V, I> factory = def.getIndexFactory();
-    I searchIndex = factory.create(search.schema);
-    IndexCollection<K, V, I> indexes = def.getIndexCollection();
+    markNotReady(cfg, versions.values(), write);
+    LuceneChangeIndex searchIndex =
+        (LuceneChangeIndex) indexFactory.create(search.schema);
     indexes.setSearchIndex(searchIndex);
-    for (Version<V> v : write) {
+    for (Version v : write) {
       if (v.schema != null) {
         if (v.version != search.version) {
-          indexes.addWriteIndex(factory.create(v.schema));
+          indexes.addWriteIndex(indexFactory.create(v.schema));
+        } else {
+          indexes.addWriteIndex(searchIndex);
         }
-      } else {
-        indexes.addWriteIndex(searchIndex);
       }
     }
 
-    // TODO: include index name.
-    markNotReady(cfg, versions.values(), write);
-
     int latest = write.get(0).version;
     if (onlineUpgrade && latest != search.version) {
-      OnlineReindexer<K, V, I> reindexer = new OnlineReindexer<>(def, latest);
-      synchronized (this) {
-        if (!reindexers.containsKey(def.getName())) {
-          reindexers.put(def.getName(), reindexer);
-          reindexer.start();
-        }
-      }
+      reindexer = new OnlineReindexer<>(changeDef, latest);
+      reindexer.start();
     }
   }
 
@@ -200,11 +186,10 @@
    * @return true if started, otherwise false.
    * @throws ReindexerAlreadyRunningException
    */
-  public synchronized boolean startReindexer(String name)
+  public synchronized boolean startReindexer()
       throws ReindexerAlreadyRunningException {
-    OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name);
-    validateReindexerNotRunning(reindexer);
-    if (!isCurrentIndexVersionLatest(name, reindexer)) {
+    validateReindexerNotRunning();
+    if (!isCurrentIndexVersionLatest()) {
       reindexer.start();
       return true;
     }
@@ -217,56 +202,49 @@
    * @return true if index was activate, otherwise false.
    * @throws ReindexerAlreadyRunningException
    */
-  public synchronized boolean activateLatestIndex(String name)
+  public synchronized boolean activateLatestIndex()
       throws ReindexerAlreadyRunningException {
-    OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name);
-    validateReindexerNotRunning(reindexer);
-    if (!isCurrentIndexVersionLatest(name, reindexer)) {
+    validateReindexerNotRunning();
+    if (!isCurrentIndexVersionLatest()) {
       reindexer.activateIndex();
       return true;
     }
     return false;
   }
 
-  private boolean isCurrentIndexVersionLatest(
-      String name, OnlineReindexer<?, ?, ?> reindexer) {
-    int readVersion = defs.get(name).getIndexCollection().getSearchIndex()
-        .getSchema().getVersion();
+  private boolean isCurrentIndexVersionLatest() {
     return reindexer == null
-        || reindexer.getVersion() == readVersion;
+        || reindexer.getVersion() == indexes.getSearchIndex().getSchema()
+            .getVersion();
   }
 
-  private static void validateReindexerNotRunning(
-      OnlineReindexer<?, ?, ?> reindexer)
+  private void validateReindexerNotRunning()
       throws ReindexerAlreadyRunningException {
     if (reindexer != null && reindexer.isRunning()) {
       throw new ReindexerAlreadyRunningException();
     }
   }
 
-  private <K, V, I extends Index<K, V>> TreeMap<Integer, Version<V>>
-      scanVersions(IndexDefinition<K, V, I> def, Config cfg) {
-    TreeMap<Integer, Version<V>> versions = Maps.newTreeMap();
-    for (Schema<V> schema : def.getSchemas().values()) {
-      // This part is Lucene-specific.
-      Path p = getDir(sitePaths, def.getName(), schema);
+  private TreeMap<Integer, Version> scanVersions(Config cfg) {
+    TreeMap<Integer, Version> versions = Maps.newTreeMap();
+    for (Schema<ChangeData> schema : changeDef.getSchemas().values()) {
+      Path p = getDir(sitePaths, CHANGES_PREFIX, schema);
       boolean isDir = Files.isDirectory(p);
       if (Files.exists(p) && !isDir) {
         log.warn("Not a directory: %s", p.toAbsolutePath());
       }
       int v = schema.getVersion();
-      versions.put(v, new Version<>(schema, v, isDir, getReady(cfg, v)));
+      versions.put(v, new Version(schema, v, isDir, getReady(cfg, v)));
     }
 
-    String prefix = def.getName() + "_";
     try (DirectoryStream<Path> paths =
         Files.newDirectoryStream(sitePaths.index_dir)) {
       for (Path p : paths) {
         String n = p.getFileName().toString();
-        if (!n.startsWith(prefix)) {
+        if (!n.startsWith(CHANGES_PREFIX)) {
           continue;
         }
-        String versionStr = n.substring(prefix.length());
+        String versionStr = n.substring(CHANGES_PREFIX.length());
         Integer v = Ints.tryParse(versionStr);
         if (v == null || versionStr.length() != 4) {
           log.warn("Unrecognized version in index directory: {}",
@@ -274,7 +252,7 @@
           continue;
         }
         if (!versions.containsKey(v)) {
-          versions.put(v, new Version<V>(null, v, true, getReady(cfg, v)));
+          versions.put(v, new Version(null, v, true, getReady(cfg, v)));
         }
       }
     } catch (IOException e) {
@@ -283,10 +261,10 @@
     return versions;
   }
 
-  private <V> void markNotReady(FileBasedConfig cfg, Iterable<Version<V>> versions,
-      Collection<Version<V>> inUse) {
+  private void markNotReady(FileBasedConfig cfg, Iterable<Version> versions,
+      Collection<Version> inUse) {
     boolean dirty = false;
-    for (Version<V> v : versions) {
+    for (Version v : versions) {
       if (!inUse.contains(v) && v.exists) {
         setReady(cfg, v.version, false);
         dirty = true;
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
similarity index 96%
rename from gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNotedb.java
rename to gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
index 2c8499a..297473d 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
@@ -46,7 +46,6 @@
 import com.google.gerrit.server.index.DummyIndexModule;
 import com.google.gerrit.server.index.change.ReindexAfterUpdate;
 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;
@@ -75,9 +74,9 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-public class RebuildNotedb extends SiteProgram {
+public class RebuildNoteDb extends SiteProgram {
   private static final Logger log =
-      LoggerFactory.getLogger(RebuildNotedb.class);
+      LoggerFactory.getLogger(RebuildNoteDb.class);
 
   @Option(name = "--threads",
       usage = "Number of threads to use for rebuilding NoteDb")
@@ -121,14 +120,14 @@
     sysInjector = createSysInjector();
     sysInjector.injectMembers(this);
     if (!notesMigration.enabled()) {
-      die("Notedb is not enabled.");
+      die("NoteDb is not enabled.");
     }
     LifecycleManager sysManager = new LifecycleManager();
     sysManager.add(sysInjector);
     sysManager.start();
 
     ListeningExecutorService executor = newExecutor();
-    System.out.println("Rebuilding the notedb");
+    System.out.println("Rebuilding the NoteDb");
 
     Multimap<Project.NameKey, Change.Id> changesByProject =
         getChangesByProject();
@@ -171,7 +170,7 @@
                 }
               }));
         } catch (Exception e) {
-          log.error("Error rebuilding notedb", e);
+          log.error("Error rebuilding NoteDb", e);
           ok.set(false);
           break;
         }
@@ -215,7 +214,6 @@
       public void configure() {
         install(dbInjector.getInstance(BatchProgramModule.class));
         install(SearchingChangeCacheImpl.module());
-        install(new NoteDbModule());
         DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(
             ReindexAfterUpdate.class);
         install(new DummyIndexModule());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
index 6599419..90c8f9f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -14,45 +14,38 @@
 
 package com.google.gerrit.pgm;
 
+import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
 import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.Die;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.util.BatchProgramModule;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.pgm.util.ThreadLimiter;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ScanningChangeCacheImpl;
+import com.google.gerrit.server.index.Index;
+import com.google.gerrit.server.index.IndexDefinition;
 import com.google.gerrit.server.index.IndexModule;
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.index.SiteIndexer;
-import com.google.gerrit.server.index.change.AllChangesIndexer;
-import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Module;
 
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.util.io.NullOutputStream;
 import org.kohsuke.args4j.Option;
 
+import java.io.IOException;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 public class Reindex extends SiteProgram {
@@ -74,16 +67,7 @@
   private Config globalConfig;
 
   @Inject
-  private AllChangesIndexer batchIndexer;
-
-  @Inject
-  private ChangeIndexCollection changeIndexes;
-
-  @Inject
-  private GitRepositoryManager repoManager;
-
-  @Inject
-  private ProjectCache projectCache;
+  private Collection<IndexDefinition<?, ?, ?>> indexDefs;
 
   @Override
   public int run() throws Exception {
@@ -105,19 +89,18 @@
     sysManager.start();
     sysInjector.injectMembers(this);
 
-    ChangeIndex index = changeIndexes.getSearchIndex();
-    int result = 0;
     try {
-      index.markReady(false);
-      index.deleteAll();
-      result = indexAll(index);
-      index.markReady(true);
+      boolean ok = true;
+      for (IndexDefinition<?, ?, ?> def : indexDefs) {
+        ok &= reindex(def);
+      }
+      return ok ? 0 : 1;
     } catch (Exception e) {
       throw die(e.getMessage(), e);
+    } finally {
+      sysManager.stop();
+      dbManager.stop();
     }
-    sysManager.stop();
-    dbManager.stop();
-    return result;
   }
 
   private void checkNotSlaveMode() throws Die {
@@ -132,16 +115,16 @@
       versions.put(ChangeSchemaDefinitions.INSTANCE.getName(), changesVersion);
     }
     List<Module> modules = Lists.newArrayList();
-    Module changeIndexModule;
+    Module indexModule;
     switch (IndexModule.getIndexType(dbInjector)) {
       case LUCENE:
-        changeIndexModule = LuceneIndexModule.singleVersionWithExplicitVersions(
+        indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(
             versions, threads);
         break;
       default:
         throw new IllegalStateException("unsupported index.type");
     }
-    modules.add(changeIndexModule);
+    modules.add(indexModule);
     // Scan changes from git instead of relying on the secondary index, as we
     // will have just deleted the old (possibly corrupt) index.
     modules.add(ScanningChangeCacheImpl.module());
@@ -161,28 +144,25 @@
     globalConfig.setLong("cache", "changes", "maximumWeight", 0);
   }
 
-  private int indexAll(ChangeIndex index) throws Exception {
-    ProgressMonitor pm = new TextProgressMonitor();
-    pm.start(1);
-    pm.beginTask("Collecting projects", ProgressMonitor.UNKNOWN);
-    Set<Project.NameKey> projects = Sets.newTreeSet();
-    int changeCount = 0;
-    for (Project.NameKey project : projectCache.all()) {
-      try (Repository repo = repoManager.openRepository(project)) {
-        changeCount += ChangeNotes.Factory.scan(repo).size();
-      }
-      projects.add(project);
-      pm.update(1);
-    }
-    pm.endTask();
+  private <K, V, I extends Index<K, V>> boolean reindex(
+      IndexDefinition<K, V, I> def) throws IOException {
+    I index = def.getIndexCollection().getSearchIndex();
+    checkNotNull(index,
+        "no active search index configured for %s", def.getName());
+    index.markReady(false);
+    index.deleteAll();
 
-    SiteIndexer.Result result = batchIndexer.setTotalWork(changeCount)
-        .setProgressOut(System.err)
-        .setVerboseOut(verbose ? System.out : NullOutputStream.INSTANCE)
-        .indexAll(index, projects);
+    SiteIndexer<K, V, I> siteIndexer = def.getSiteIndexer();
+    siteIndexer.setProgressOut(System.err);
+    siteIndexer.setVerboseOut(verbose ? System.out : NullOutputStream.INSTANCE);
+    SiteIndexer.Result result = siteIndexer.indexAll(index);
     int n = result.doneCount() + result.failedCount();
     double t = result.elapsed(TimeUnit.MILLISECONDS) / 1000d;
-    System.out.format("Reindexed %d changes in %.01fs (%.01f/s)\n", n, t, n/t);
-    return result.success() ? 0 : 1;
+    System.out.format("Reindexed %d documents in %s index in %.01fs (%.01f/s)\n",
+        n, def.getName(), t, n/t);
+    if (result.success()) {
+      index.markReady(true);
+    }
+    return result.success();
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 6270a15..9060bf0 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -105,6 +105,7 @@
     extractMailExample("ChangeSubject.vm");
     extractMailExample("Comment.vm");
     extractMailExample("CommentFooter.vm");
+    extractMailExample("DeleteVote.vm");
     extractMailExample("Footer.vm");
     extractMailExample("Merged.vm");
     extractMailExample("MergeFail.vm");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
index dfe0403..7b0e4da 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ThreadLimiter.java
@@ -24,7 +24,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-// TODO(dborowitz): Not necessary once we switch to notedb.
+// TODO(dborowitz): Not necessary once we switch to NoteDb.
 /** Utility to limit threads used by a batch program. */
 public class ThreadLimiter {
   private static final Logger log =
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
index 7e6f943..9862d87 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -273,7 +273,7 @@
 # Add Gerrit properties to Java VM options.
 #####################################################
 
-GERRIT_OPTIONS=`get_config --get-all container.javaOptions`
+GERRIT_OPTIONS=`get_config --get-all container.javaOptions | tr '\n' ' '`
 if test -n "$GERRIT_OPTIONS" ; then
   JAVA_OPTIONS="$JAVA_OPTIONS $GERRIT_OPTIONS"
 fi
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index bd7b7c1..c04b729 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -148,6 +148,10 @@
     return key;
   }
 
+  public PatchSet.Id getPatchSetId() {
+    return key.getParentKey().getParentKey();
+  }
+
   public int getLine() {
     return lineNbr;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index 1b00485..d2250cf 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -46,7 +46,7 @@
   /** A change starred by a user */
   public static final String REFS_STARRED_CHANGES = "refs/starred-changes/";
 
-  /** Sequence counters in notedb. */
+  /** Sequence counters in NoteDb. */
   public static final String REFS_SEQUENCES = "refs/sequences/";
 
   /**
@@ -61,7 +61,7 @@
    */
   public static final String REFS_CACHE_AUTOMERGE = "refs/cache-automerge/";
 
-  /** Suffix of a meta ref in the notedb. */
+  /** Suffix of a meta ref in the NoteDb. */
   public static final String META_SUFFIX = "/meta";
 
   public static final String EDIT_PREFIX = "edit-";
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 6232f2a..5d073e2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -96,8 +96,7 @@
   @Relation(id = 26)
   PatchLineCommentAccess patchComments();
 
-  @Relation(id = 28)
-  SubmoduleSubscriptionAccess submoduleSubscriptions();
+  // Deleted @Relation(id = 28)
 
   @Relation(id = 29)
   AccountGroupByIdAccess accountGroupById();
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 7522c2d..deecba9 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -154,11 +154,6 @@
   }
 
   @Override
-  public SubmoduleSubscriptionAccess submoduleSubscriptions() {
-    return delegate.submoduleSubscriptions();
-  }
-
-  @Override
   public AccountGroupByIdAccess accountGroupById() {
     return delegate.accountGroupById();
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
deleted file mode 100644
index b25e406..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (C) 2011 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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface SubmoduleSubscriptionAccess extends
-    Access<SubmoduleSubscription, SubmoduleSubscription.Key> {
-  @Override
-  @PrimaryKey("key")
-  SubmoduleSubscription get(SubmoduleSubscription.Key key) throws OrmException;
-
-  @Query("WHERE key.superProject = ?")
-  ResultSet<SubmoduleSubscription> bySuperProject(Branch.NameKey superProject)
-      throws OrmException;
-
-  /**
-   * Fetches all {@code SubmoduleSubscription}s in which some branch of
-   * {@code superProject} subscribes a branch.
-   *
-   * Use {@link #bySuperProject(Branch.NameKey)} to fetch for a branch instead
-   * of a project.
-   *
-   * @param superProject the project to fetch subscriptions for
-   * @return {@code SubmoduleSubscription}s that are subscribed by some
-   * branch of {@code superProject}.
-   * @throws OrmException
-   */
-  @Query("WHERE key.superProject.projectName = ?")
-  ResultSet<SubmoduleSubscription> bySuperProjectProject(Project.NameKey superProject)
-      throws OrmException;
-
-  @Query("WHERE submodule = ?")
-  ResultSet<SubmoduleSubscription> bySubmodule(Branch.NameKey submodule)
-      throws OrmException;
-
-  /**
-   * Fetches all {@code SubmoduleSubscription}s in which some branch of
-   * {@code submodule} is subscribed.
-   *
-   * Use {@link #bySubmodule(Branch.NameKey)} to fetch for a branch instead of
-   * a project.
-   *
-   * @param submodule the project to fetch subscriptions for.
-   * @return {@code SubmoduleSubscription}s that subscribe some branch of
-   * {@code submodule}.
-   * @throws OrmException
-   */
-  @Query("WHERE submodule.projectName = ?")
-  ResultSet<SubmoduleSubscription> bySubmoduleProject(Project.NameKey submodule)
-      throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 1162a5f..b8ebdde 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -93,9 +93,3 @@
 
 CREATE INDEX starred_changes_byChange
 ON starred_changes (change_id);
-
--- *********************************************************************
--- SubmoduleSubscriptionAccess
-
-CREATE INDEX submodule_subscr_acc_byS
-ON submodule_subscriptions (submodule_project_name, submodule_branch_name);
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
index 258b7be..d7135a2 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -103,11 +103,3 @@
 CREATE INDEX starred_changes_byChange
 ON starred_changes (change_id)
 #
-
--- *********************************************************************
--- SubmoduleSubscriptionAccess
-
-CREATE INDEX submod_subscr_ac_bySubscription
-ON submodule_subscriptions (submodule_project_name, submodule_branch_name)
-#
-
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 1fe7dce..b0b6b6f 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -143,8 +143,3 @@
 CREATE INDEX starred_changes_byChange
 ON starred_changes (change_id);
 
--- *********************************************************************
--- SubmoduleSubscriptionAccess
-
-CREATE INDEX submodule_subscr_acc_byS
-ON submodule_subscriptions (submodule_project_name, submodule_branch_name);
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 be5d1c8..b8d609d 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
@@ -17,7 +17,6 @@
 import com.google.common.base.Optional;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
-import com.google.common.base.Throwables;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.common.data.LabelType;
@@ -845,7 +844,7 @@
                 return eventFactory.asPatchSetAttribute(
                     revWalk, change, patchSet);
               } catch (IOException e) {
-                throw Throwables.propagate(e);
+                throw new RuntimeException(e);
               }
             }
           });
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index 0361374..dba723f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -55,7 +55,7 @@
  * <p>
  * The result of a copy may either be stored, as when stamping approvals in the
  * database at submit time, or refreshed on demand, as when reading approvals
- * from the notedb.
+ * from the NoteDb.
  */
 @Singleton
 public class ApprovalCopier {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
index cb7e65b..ecd9027 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
@@ -313,7 +314,7 @@
         bru.addCommand(new ReceiveCommand(
             ref.getObjectId(), ObjectId.zeroId(), ref.getName()));
       }
-      bru.setRefLogMessage("Delete drafts from notedb", false);
+      bru.setRefLogMessage("Delete drafts from NoteDb", false);
       bru.execute(rw, NullProgressMonitor.INSTANCE);
       for (ReceiveCommand cmd : bru.getCommands()) {
         if (cmd.getResult() != ReceiveCommand.Result.OK) {
@@ -353,13 +354,18 @@
 
   public static RevId setCommentRevId(PatchLineComment c,
       PatchListCache cache, Change change, PatchSet ps) throws OrmException {
+    checkArgument(c.getPatchSetId().equals(ps.getId()),
+        "cannot set RevId for patch set %s on comment %s", ps.getId(), c);
     if (c.getRevId() == null) {
       try {
-        // TODO(dborowitz): Bypass cache if side is REVISION.
-        PatchList patchList = cache.get(change, ps);
-        c.setRevId((c.getSide() == (short) 0)
-          ? new RevId(ObjectId.toString(patchList.getOldId()))
-          : new RevId(ObjectId.toString(patchList.getNewId())));
+        if (Side.fromShort(c.getSide()) == Side.REVISION) {
+          c.setRevId(ps.getRevision());
+        } else {
+          PatchList patchList = cache.get(change, ps);
+          c.setRevId((c.getSide() == (short) 0)
+            ? new RevId(ObjectId.toString(patchList.getOldId()))
+            : new RevId(ObjectId.toString(patchList.getNewId())));
+        }
       } catch (PatchListNotAvailableException e) {
         throw new OrmException(e);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
index 134189e..1bbd735 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.server.notedb.PatchSetState.PUBLISHED;
 
 import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -72,6 +73,20 @@
     return notes.load().getPatchSets().values();
   }
 
+  public ImmutableMap<PatchSet.Id, PatchSet> byChangeAsMap(ReviewDb db,
+        ChangeNotes notes) throws OrmException {
+    if (!migration.readChanges()) {
+      ImmutableMap.Builder<PatchSet.Id, PatchSet> result =
+          ImmutableMap.builder();
+      for (PatchSet ps : ChangeUtil.PS_ID_ORDER.sortedCopy(
+          db.patchSets().byChange(notes.getChangeId()))) {
+        result.put(ps.getId(), ps);
+      }
+      return result.build();
+    }
+    return notes.load().getPatchSets();
+  }
+
   public PatchSet insert(ReviewDb db, RevWalk rw, ChangeUpdate update,
       PatchSet.Id psId, ObjectId commit, boolean draft,
       List<String> groups, String pushCertificate)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
index b3063b4..41a965f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
@@ -15,10 +15,6 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.NotesMigration;
-import com.google.gerrit.server.notedb.RepoSequence;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -27,26 +23,16 @@
 @Singleton
 public class Sequences {
   private final Provider<ReviewDb> db;
-  private final NotesMigration migration;
-  private final RepoSequence changeSeq;
 
   @Inject
-  Sequences(Provider<ReviewDb> db,
-      NotesMigration migration,
-      GitRepositoryManager repoManager,
-      AllProjectsName allProjects) {
+  Sequences(Provider<ReviewDb> db) {
     this.db = db;
-    this.migration = migration;
-    changeSeq = new RepoSequence(
-        repoManager, allProjects, "changes", ReviewDb.FIRST_CHANGE_ID, 100);
   }
 
   @SuppressWarnings("deprecation")
   public int nextChangeId() throws OrmException {
-    if (migration.readChanges()) {
-      return changeSeq.next();
-    } else {
-      return db.get().nextChangeId();
-    }
+    // TODO(dborowitz): Use repo sequence when we have ability to turn off
+    // ReviewDb entirely. Until then it's simpler to just keep using ReviewDb.
+    return db.get().nextChangeId();
   }
 }
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 a9bf220..274826a 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.api.changes.Changes;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.api.changes.MoveInput;
 import com.google.gerrit.extensions.api.changes.RestoreInput;
 import com.google.gerrit.extensions.api.changes.RevertInput;
 import com.google.gerrit.extensions.api.changes.ReviewerApi;
@@ -43,6 +44,7 @@
 import com.google.gerrit.server.change.GetTopic;
 import com.google.gerrit.server.change.ListChangeComments;
 import com.google.gerrit.server.change.ListChangeDrafts;
+import com.google.gerrit.server.change.Move;
 import com.google.gerrit.server.change.PostHashtags;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.PublishDraftPatchSet;
@@ -96,6 +98,7 @@
   private final ListChangeDrafts listDrafts;
   private final Check check;
   private final ChangeEdits.Detail editDetail;
+  private final Move move;
 
   @Inject
   ChangeApiImpl(Provider<CurrentUser> user,
@@ -121,6 +124,7 @@
       ListChangeDrafts listDrafts,
       Check check,
       ChangeEdits.Detail editDetail,
+      Move move,
       @Assisted ChangeResource change) {
     this.user = user;
     this.changeApi = changeApi;
@@ -145,6 +149,7 @@
     this.listDrafts = listDrafts;
     this.check = check;
     this.editDetail = editDetail;
+    this.move = move;
     this.change = change;
   }
 
@@ -212,6 +217,22 @@
   }
 
   @Override
+  public void move(String destination) throws RestApiException {
+    MoveInput in = new MoveInput();
+    in.destination_branch = destination;
+    move(in);
+  }
+
+  @Override
+  public void move(MoveInput in) throws RestApiException {
+    try {
+      move.apply(change, in);
+    } catch (OrmException | UpdateException e) {
+      throw new RestApiException("Cannot move change", e);
+    }
+  }
+
+  @Override
   public ChangeApi revert() throws RestApiException {
     return revert(new RevertInput());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index c8905ee..59c25e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -392,11 +392,11 @@
       }
     }
 
-    /** For labels that are not set in this operation, show the "current" value
+    /* For labels that are not set in this operation, show the "current" value
      * of 0, and no oldValue as the value was not modified by this operation.
      * For labels that are set in this operation, the value was modified, so
      * show a transition from an oldValue of 0 to the new value.
-     **/
+     */
     if (runHooks) {
       ReviewDb db = ctx.getDb();
       hooks.doPatchsetCreatedHook(change, patchSet, db);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index b3e7c6b..3171245 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -97,7 +97,7 @@
       noteId = null; // This ETag will be invalidated if it loads next time.
     }
     hashObjectId(h, noteId, buf);
-    // TODO(dborowitz): Include more notedb and other related refs, e.g. drafts
+    // TODO(dborowitz): Include more NoteDb and other related refs, e.g. drafts
     // and edits.
 
     for (ProjectState p : control.getProjectControl().getProjectState().tree()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
index 20165f4..465ce95 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -68,7 +68,7 @@
 
   private void checkEnabled() throws NotImplementedException {
     if (notesMigration.readChanges()) {
-      throw new NotImplementedException("check not implemented for notedb");
+      throw new NotImplementedException("check not implemented for NoteDb");
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 5fda005..5bf0781 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -212,7 +212,7 @@
 
   private void checkImpl() {
     checkState(!notesMigration.readChanges(),
-        "ConsistencyChecker for notedb not yet implemented");
+        "ConsistencyChecker for NoteDb not yet implemented");
     checkOwner();
     checkCurrentPatchSetEntity();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
index fc2891c..f1dcee5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChangeOp.java
@@ -50,7 +50,7 @@
 
   static ReviewDb unwrap(ReviewDb db) {
     // This is special. We want to delete exactly the rows that are present in
-    // the database, even when reading everything else from notedb, so we need
+    // the database, even when reading everything else from NoteDb, so we need
     // to bypass the write-only wrapper.
     if (db instanceof BatchUpdateReviewDb) {
       db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
@@ -108,7 +108,7 @@
           db.accountPatchReviews().byPatchSet(ps.getId()));
     }
 
-    // Only delete from reviewdb here; deletion from notedb is handled in
+    // Only delete from ReviewDb here; deletion from NoteDb is handled in
     // BatchUpdate.
     db.patchComments().delete(db.patchComments().byChange(id));
     db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index 8567951..59d4290 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -136,7 +136,7 @@
 
     private void deleteDraftPatchSet(PatchSet patchSet, ChangeContext ctx)
         throws OrmException {
-      // For notedb itself, no need to delete these entities, as they are
+      // For NoteDb itself, no need to delete these entities, as they are
       // automatically filtered out when patch sets are deleted.
       psUtil.delete(ctx.getDb(), ctx.getUpdate(patchSet.getId()), patchSet);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index 44f2a14..0ded3a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -14,7 +14,10 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -30,40 +33,60 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.DeleteVote.Input;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
 import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.mail.DeleteVoteSender;
+import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 @Singleton
 public class DeleteVote implements RestModifyView<VoteResource, Input> {
+  private static final Logger log = LoggerFactory.getLogger(DeleteVote.class);
+
   public static class Input {
   }
 
   private final Provider<ReviewDb> db;
   private final BatchUpdate.Factory batchUpdateFactory;
   private final ApprovalsUtil approvalsUtil;
+  private final PatchSetUtil psUtil;
   private final ChangeMessagesUtil cmUtil;
   private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeHooks hooks;
+  private final DeleteVoteSender.Factory deleteVoteSenderFactory;
 
   @Inject
   DeleteVote(Provider<ReviewDb> db,
       BatchUpdate.Factory batchUpdateFactory,
       ApprovalsUtil approvalsUtil,
+      PatchSetUtil psUtil,
       ChangeMessagesUtil cmUtil,
-      IdentifiedUser.GenericFactory userFactory) {
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeHooks hooks,
+      DeleteVoteSender.Factory deleteVoteSenderFactory) {
     this.db = db;
     this.batchUpdateFactory = batchUpdateFactory;
     this.approvalsUtil = approvalsUtil;
+    this.psUtil = psUtil;
     this.cmUtil = cmUtil;
     this.userFactory = userFactory;
+    this.hooks = hooks;
+    this.deleteVoteSenderFactory = deleteVoteSenderFactory;
   }
 
   @Override
@@ -84,6 +107,11 @@
   private class Op extends BatchUpdate.Op {
     private final Account.Id accountId;
     private final String label;
+    private ChangeMessage changeMessage;
+    private Change change;
+    private PatchSet ps;
+    private Map<String, Short> newApprovals = new HashMap<>();
+    private Map<String, Short> oldApprovals = new HashMap<>();
 
     private Op(Account.Id accountId, String label) {
       this.accountId = accountId;
@@ -93,17 +121,36 @@
     @Override
     public boolean updateChange(ChangeContext ctx)
         throws OrmException, AuthException, ResourceNotFoundException {
-      IdentifiedUser user = ctx.getUser().asIdentifiedUser();
-      Change change = ctx.getChange();
       ChangeControl ctl = ctx.getControl();
+      change = ctl.getChange();
       PatchSet.Id psId = change.currentPatchSetId();
+      ps = psUtil.current(db.get(), ctl.getNotes());
 
       PatchSetApproval psa = null;
       StringBuilder msg = new StringBuilder();
+
+      // get all of the current approvals
+      LabelTypes labelTypes = ctx.getControl().getLabelTypes();
+      Map<String, Short> currentApprovals = new HashMap<>();
+      for (LabelType lt : labelTypes.getLabelTypes()) {
+        currentApprovals.put(lt.getName(), (short) 0);
+        for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
+            ctx.getDb(), ctl, psId, accountId)) {
+          if (lt.getLabelId().equals(a.getLabelId())) {
+            currentApprovals.put(lt.getName(), a.getValue());
+          }
+        }
+      }
+      // removing votes so we need to determine the new set of approval scores
+      newApprovals.putAll(currentApprovals);
       for (PatchSetApproval a : approvalsUtil.byPatchSetUser(
             ctx.getDb(), ctl, psId, accountId)) {
         if (ctl.canRemoveReviewer(a)) {
           if (a.getLabel().equals(label)) {
+            // set the approval to 0 if vote is being removed
+            newApprovals.put(a.getLabel(), (short) 0);
+            // set old value only if the vote changed
+            oldApprovals.put(a.getLabel(), a.getValue());
             msg.append("Removed ")
                 .append(a.getLabel()).append(formatLabelValue(a.getValue()))
                 .append(" by ").append(userFactory.create(a.getAccountId())
@@ -125,10 +172,10 @@
       ctx.getDb().patchSetApprovals().update(Collections.singleton(psa));
 
       if (msg.length() > 0) {
-        ChangeMessage changeMessage =
+        changeMessage =
             new ChangeMessage(new ChangeMessage.Key(change.getId(),
                 ChangeUtil.messageUUID(ctx.getDb())),
-                user.getAccountId(),
+                ctx.getUser().asIdentifiedUser().getAccountId(),
                 ctx.getWhen(),
                 change.currentPatchSetId());
         changeMessage.setMessage(msg.toString());
@@ -137,6 +184,31 @@
       }
       return true;
     }
+
+    @Override
+    public void postUpdate(Context ctx) {
+      if (changeMessage == null) {
+        return;
+      }
+
+      IdentifiedUser user = ctx.getUser().asIdentifiedUser();
+      try {
+        ReplyToChangeSender cm = deleteVoteSenderFactory.create(
+            ctx.getProject(), change.getId());
+        cm.setFrom(user.getAccountId());
+        cm.setChangeMessage(changeMessage);
+        cm.send();
+      } catch (Exception e) {
+        log.error("Cannot email update for change " + change.getId(), e);
+      }
+
+      try {
+        hooks.doCommentAddedHook(change, user.getAccount(), ps,
+            changeMessage.getMessage(), newApprovals, oldApprovals, ctx.getDb());
+      } catch (OrmException e) {
+        log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
+      }
+    }
   }
 
   private static String formatLabelValue(short value) {
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 37f15bd..4c9c0bf 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
@@ -70,6 +70,8 @@
     get(CHANGE_KIND, "submitted_together").to(SubmittedTogether.class);
     post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
     post(CHANGE_KIND, "index").to(Index.class);
+    post(CHANGE_KIND, "rebuild.notedb").to(Rebuild.class);
+    post(CHANGE_KIND, "move").to(Move.class);
 
     post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
     get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
new file mode 100644
index 0000000..f9dc0695
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -0,0 +1,197 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.gerrit.server.query.change.ChangeData.asChanges;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.MoveInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+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.Change.Status;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+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 java.io.IOException;
+
+@Singleton
+public class Move implements RestModifyView<ChangeResource, MoveInput> {
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeJson.Factory json;
+  private final GitRepositoryManager repoManager;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final ChangeMessagesUtil cmUtil;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final PatchSetUtil psUtil;
+
+  @Inject
+  Move(Provider<ReviewDb> dbProvider,
+      ChangeJson.Factory json,
+      GitRepositoryManager repoManager,
+      Provider<InternalChangeQuery> queryProvider,
+      ChangeMessagesUtil cmUtil,
+      BatchUpdate.Factory batchUpdateFactory,
+      PatchSetUtil psUtil) {
+    this.dbProvider = dbProvider;
+    this.json = json;
+    this.repoManager = repoManager;
+    this.queryProvider = queryProvider;
+    this.cmUtil = cmUtil;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.psUtil = psUtil;
+  }
+
+  @Override
+  public ChangeInfo apply(ChangeResource req, MoveInput input)
+      throws RestApiException, OrmException, UpdateException {
+    ChangeControl control = req.getControl();
+    input.destination_branch = RefNames.fullName(input.destination_branch);
+    if (!control.canMoveTo(input.destination_branch, dbProvider.get())) {
+      throw new AuthException("Move not permitted");
+    }
+
+    try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
+        req.getChange().getProject(), control.getUser(), TimeUtil.nowTs())) {
+      u.addOp(req.getChange().getId(), new Op(control, input));
+      u.execute();
+    }
+
+    return json.create(ChangeJson.NO_OPTIONS).format(req.getChange());
+  }
+
+  private class Op extends BatchUpdate.Op {
+    private final MoveInput input;
+    private final IdentifiedUser caller;
+
+    private Change change;
+    private Branch.NameKey newDestKey;
+
+    public Op(ChangeControl ctl, MoveInput input) {
+      this.input = input;
+      this.caller = ctl.getUser().asIdentifiedUser();
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx) throws OrmException,
+        ResourceConflictException, RepositoryNotFoundException, IOException {
+      change = ctx.getChange();
+      if (change.getStatus() != Status.NEW
+          && change.getStatus() != Status.DRAFT) {
+        throw new ResourceConflictException("Change is " + status(change));
+      }
+
+      Project.NameKey projectKey = change.getProject();
+      newDestKey = new Branch.NameKey(projectKey, input.destination_branch);
+      Branch.NameKey changePrevDest = change.getDest();
+      if (changePrevDest.equals(newDestKey)) {
+        throw new ResourceConflictException(
+            "Change is already destined for the specified branch");
+      }
+
+      final PatchSet.Id patchSetId = change.currentPatchSetId();
+      try (Repository repo = repoManager.openRepository(projectKey);
+          RevWalk revWalk = new RevWalk(repo)) {
+        RevCommit currPatchsetRevCommit = revWalk.parseCommit(
+            ObjectId.fromString(psUtil.current(ctx.getDb(), ctx.getNotes())
+                .getRevision().get()));
+        if (currPatchsetRevCommit.getParentCount() > 1) {
+          throw new ResourceConflictException("Merge commit cannot be moved");
+        }
+
+        ObjectId refId = repo.resolve(input.destination_branch);
+        // Check if destination ref exists in project repo
+        if (refId == null) {
+          throw new ResourceConflictException(
+              "Destination " + input.destination_branch + " not found in the project");
+        }
+        RevCommit refCommit = revWalk.parseCommit(refId);
+        if (revWalk.isMergedInto(currPatchsetRevCommit, refCommit)) {
+          throw new ResourceConflictException(
+              "Current patchset revision is reachable from tip of "
+                  + input.destination_branch);
+        }
+      }
+
+      Change.Key changeKey = change.getKey();
+      if (!asChanges(queryProvider.get().byBranchKey(newDestKey, changeKey))
+          .isEmpty()) {
+        throw new ResourceConflictException(
+            "Destination " + newDestKey.getShortName()
+                + " has a different change with same change key " + changeKey);
+      }
+
+      if (!change.currentPatchSetId().equals(patchSetId)) {
+        throw new ResourceConflictException("Patch set is not current");
+      }
+
+      ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
+      update.setBranch(newDestKey.get());
+      change.setDest(newDestKey);
+
+      StringBuilder msgBuf = new StringBuilder();
+      msgBuf.append("Change destination moved from ");
+      msgBuf.append(changePrevDest.getShortName());
+      msgBuf.append(" to ");
+      msgBuf.append(newDestKey.getShortName());
+      if (!Strings.isNullOrEmpty(input.message)) {
+        msgBuf.append("\n\n");
+        msgBuf.append(input.message);
+      }
+      ChangeMessage cmsg = new ChangeMessage(
+          new ChangeMessage.Key(change.getId(),
+              ChangeUtil.messageUUID(ctx.getDb())),
+          caller.getAccountId(), ctx.getWhen(), change.currentPatchSetId());
+      cmsg.setMessage(msgBuf.toString());
+      cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
+
+      ctx.saveChange();
+      return true;
+    }
+  }
+
+  private static String status(Change change) {
+    return change != null ? change.getStatus().name().toLowerCase() : "deleted";
+  }
+}
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 fbfcc87..7492969 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
@@ -23,6 +23,7 @@
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
@@ -469,18 +470,15 @@
           del.addAll(drafts.values());
           break;
         case PUBLISH:
-        case PUBLISH_ALL_REVISIONS:
           for (PatchLineComment e : drafts.values()) {
-            e.setStatus(PatchLineComment.Status.PUBLISHED);
-            e.setWrittenOn(ctx.getWhen());
-            setCommentRevId(e, patchListCache, ctx.getChange(), ps);
-            ups.add(e);
+            ups.add(publishComment(ctx, e, ps));
           }
           break;
+        case PUBLISH_ALL_REVISIONS:
+          publishAllRevisions(ctx, drafts, ups);
+          break;
       }
       ChangeUpdate u = ctx.getUpdate(psId);
-      // TODO(dborowitz): Currently doesn't work for PUBLISH_ALL_REVISIONS with
-      // notedb.
       plcUtil.deleteComments(ctx.getDb(), u, del);
       plcUtil.putComments(ctx.getDb(), u, ups);
       comments.addAll(ups);
@@ -526,11 +524,44 @@
       return labels;
     }
 
+    private PatchLineComment publishComment(ChangeContext ctx,
+        PatchLineComment c, PatchSet ps) throws OrmException {
+      c.setStatus(PatchLineComment.Status.PUBLISHED);
+      c.setWrittenOn(ctx.getWhen());
+      setCommentRevId(c, patchListCache, ctx.getChange(), checkNotNull(ps));
+      return c;
+    }
+
+    private void publishAllRevisions(ChangeContext ctx,
+        Map<String, PatchLineComment> drafts, List<PatchLineComment> ups)
+        throws OrmException {
+      boolean needOtherPatchSets = false;
+      for (PatchLineComment c : drafts.values()) {
+        if (!c.getPatchSetId().equals(psId)) {
+          needOtherPatchSets = true;
+          break;
+        }
+      }
+      Map<PatchSet.Id, PatchSet> patchSets = needOtherPatchSets
+          ? psUtil.byChangeAsMap(ctx.getDb(), ctx.getNotes())
+          : ImmutableMap.of(psId, ps);
+      for (PatchLineComment e : drafts.values()) {
+        ups.add(publishComment(ctx, e, patchSets.get(e.getPatchSetId())));
+      }
+    }
+
     private boolean updateLabels(ChangeContext ctx)
         throws OrmException, ResourceConflictException {
       Map<String, Short> inLabels = MoreObjects.firstNonNull(in.labels,
           Collections.<String, Short> emptyMap());
 
+      // If no labels were modified and change is closed, abort early.
+      // This avoids trying to record a modified label caused by a user
+      // losing access to a label after the change was submitted.
+      if (inLabels.isEmpty() && ctx.getChange().getStatus().isClosed()) {
+        return false;
+      }
+
       List<PatchSetApproval> del = Lists.newArrayList();
       List<PatchSetApproval> ups = Lists.newArrayList();
       Map<String, PatchSetApproval> current = scanLabels(ctx, del);
@@ -604,10 +635,9 @@
         }
       }
 
-      if (!del.isEmpty() || !ups.isEmpty()) {
-        if (ctx.getChange().getStatus().isClosed()) {
-          throw new ResourceConflictException("change is closed");
-        }
+      if ((!del.isEmpty() || !ups.isEmpty())
+          && ctx.getChange().getStatus().isClosed()) {
+        throw new ResourceConflictException("change is closed");
       }
       forceCallerAsReviewer(ctx, current, ups, del);
       ctx.getDb().patchSetApprovals().delete(del);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebuild.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebuild.java
new file mode 100644
index 0000000..9fa966e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebuild.java
@@ -0,0 +1,68 @@
+// 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.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.Rebuild.Input;
+import com.google.gerrit.server.notedb.ChangeRebuilder;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
+
+@Singleton
+public class Rebuild implements RestModifyView<ChangeResource, Input> {
+  public static class Input {
+  }
+
+  private final Provider<ReviewDb> db;
+  private final NotesMigration migration;
+  private final ChangeRebuilder rebuilder;
+
+  @Inject
+  Rebuild(Provider<ReviewDb> db,
+      NotesMigration migration,
+      ChangeRebuilder rebuilder) {
+    this.db = db;
+    this.migration = migration;
+    this.rebuilder = rebuilder;
+  }
+
+  @Override
+  public Response<?> apply(ChangeResource rsrc, Input input)
+      throws ResourceNotFoundException, IOException, OrmException,
+      ConfigInvalidException {
+    if (!migration.writeChanges()) {
+      throw new ResourceNotFoundException();
+    }
+    try {
+      rebuilder.rebuild(db.get(), rsrc.getId());
+    } catch (NoSuchChangeException e) {
+      throw new ResourceNotFoundException(
+          IdString.fromDecoded(rsrc.getId().toString()));
+    }
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
index 685ff7b..a8fd013 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -22,7 +22,7 @@
 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.server.IdentifiedUser;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.ChangeControl;
@@ -88,8 +88,8 @@
     return getUser().getAccountId();
   }
 
-  IdentifiedUser getUser() {
-    return getControl().getUser().asIdentifiedUser();
+  CurrentUser getUser() {
+    return getControl().getUser();
   }
 
   RevisionResource doNotCache() {
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 fb69f87..0c60569 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
@@ -42,6 +42,7 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.ProjectUtil;
@@ -245,16 +246,16 @@
 
   /**
    * @param cs set of changes to be submitted at once
-   * @param identifiedUser the user who is checking to submit
+   * @param user the user who is checking to submit
    * @return a reason why any of the changes is not submittable or null
    */
   private String problemsForSubmittingChangeset(ChangeSet cs,
-      IdentifiedUser identifiedUser) {
+      CurrentUser user) {
     try {
       @SuppressWarnings("resource")
       ReviewDb db = dbProvider.get();
       for (ChangeData c : cs.changes()) {
-        ChangeControl changeControl = c.changeControl(identifiedUser);
+        ChangeControl changeControl = c.changeControl(user);
 
         if (!changeControl.isVisible(db)) {
           return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 883c57c..5a1fdc6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -84,6 +84,7 @@
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.EmailMerge;
 import com.google.gerrit.server.git.GitModule;
+import com.google.gerrit.server.git.GitModules;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReceivePackInitializer;
@@ -323,6 +324,7 @@
     factory(NotesBranchUtil.Factory.class);
     factory(SubmoduleSectionParser.Factory.class);
     factory(ReplaceOp.Factory.class);
+    factory(GitModules.Factory.class);
 
     bind(AccountManager.class);
     factory(ChangeUserName.Factory.class);
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 7357965..f18a429 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
@@ -196,6 +196,9 @@
       ChangeUpdate u = updates.get(psId);
       if (u == null) {
         u = changeUpdateFactory.create(ctl, when);
+        if (newChanges.containsKey(ctl.getId())) {
+          u.setAllowWriteToNewRef(true);
+        }
         u.setPatchSetId(psId);
         updates.put(psId, u);
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
new file mode 100644
index 0000000..9042955
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
@@ -0,0 +1,131 @@
+// 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.git;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.util.SubmoduleSectionParser;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.FileMode;
+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.eclipse.jgit.treewalk.TreeWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Loads the .gitmodules file of the specified project/branch.
+ * It can be queried which submodules this branch is subscribed to.
+ */
+public class GitModules {
+  private static final Logger log = LoggerFactory.getLogger(GitModules.class);
+
+  public interface Factory {
+    GitModules create(Branch.NameKey project, String submissionId);
+  }
+
+  private static final String GIT_MODULES = ".gitmodules";
+
+  private final String thisServer;
+  private final GitRepositoryManager repoManager;
+  private final SubmoduleSectionParser.Factory subSecParserFactory;
+  private final Branch.NameKey branch;
+  private final String submissionId;
+
+  Set<SubmoduleSubscription> subscriptions;
+
+  @AssistedInject
+  GitModules(
+      @CanonicalWebUrl @Nullable String canonicalWebUrl,
+      GitRepositoryManager repoManager,
+      SubmoduleSectionParser.Factory subSecParserFactory,
+      @Assisted Branch.NameKey branch,
+      @Assisted String submissionId) throws SubmoduleException {
+    this.repoManager = repoManager;
+    this.subSecParserFactory = subSecParserFactory;
+    this.branch = branch;
+    this.submissionId = submissionId;
+    try {
+      this.thisServer = new URI(canonicalWebUrl).getHost();
+    } catch (URISyntaxException e) {
+      throw new SubmoduleException("Incorrect Gerrit canonical web url " +
+          "provided in gerrit.config file.", e);
+    }
+  }
+
+  void load() throws IOException {
+    Project.NameKey project = branch.getParentKey();
+    logDebug("Loading .gitmodules of {} for project {}", branch, project);
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk rw = new RevWalk(repo)) {
+
+      ObjectId id = repo.resolve(branch.get());
+      if (id == null) {
+        throw new IOException("Cannot open branch " + branch.get());
+      }
+      RevCommit commit = rw.parseCommit(id);
+
+      TreeWalk tw = TreeWalk.forPath(repo, GIT_MODULES, commit.getTree());
+      if (tw == null
+          || (tw.getRawMode(0) & FileMode.TYPE_MASK) != FileMode.TYPE_FILE) {
+        return;
+      }
+
+      BlobBasedConfig bbc =
+          new BlobBasedConfig(null, repo, commit, GIT_MODULES);
+
+      subscriptions = subSecParserFactory.create(bbc, thisServer,
+          branch).parseAllSections();
+    } catch (ConfigInvalidException e) {
+      throw new IOException(
+          "Could not read .gitmodule file of super project: " +
+              branch.getParentKey(), e);
+    }
+  }
+
+  public Collection<SubmoduleSubscription> subscribedTo(Branch.NameKey src) {
+    logDebug("Checking for a subscription of " + src);
+    Collection<SubmoduleSubscription> ret = new ArrayList<>();
+    for (SubmoduleSubscription s : subscriptions) {
+      if (s.getSubmodule().equals(src)) {
+        logDebug("Found " + s);
+        ret.add(s);
+      }
+    }
+    return ret;
+  }
+
+  private void logDebug(String msg, Object... args) {
+    if (log.isDebugEnabled()) {
+      log.debug("[" + submissionId + "]" + msg, args);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 33670c6..e0c4c74 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -279,7 +279,7 @@
   @Override
   public Repository openMetadataRepository(Project.NameKey name)
       throws RepositoryNotFoundException, IOException {
-    checkState(notesMigration.readChanges(), "notedb disabled");
+    checkState(notesMigration.readChanges(), "NoteDb disabled");
     try {
       return openRepository(noteDbPath, name);
     } catch (RepositoryNotFoundException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index e213927..07aa892 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -177,13 +177,11 @@
   }
 
   private static class OpenBranch {
-    final Branch.NameKey name;
     final RefUpdate update;
     final CodeReviewCommit oldTip;
     MergeTip mergeTip;
 
     OpenBranch(OpenRepo or, Branch.NameKey name) throws IntegrationException {
-      this.name = name;
       try {
         update = or.repo.updateRef(name.get());
         if (update.getOldObjectId() != null) {
@@ -650,10 +648,6 @@
     }
 
     SubmoduleOp subOp = subOpProvider.get();
-    for (Branch.NameKey branch : branches) {
-      OpenBranch ob = getRepo(branch.getParentKey()).getBranch(branch);
-      updateSubmoduleSubscriptions(ob, subOp);
-    }
     updateSuperProjects(subOp, br.values());
   }
 
@@ -854,26 +848,12 @@
     }
   }
 
-  private void updateSubmoduleSubscriptions(OpenBranch ob, SubmoduleOp subOp) {
-    CodeReviewCommit branchTip = ob.oldTip;
-    MergeTip mergeTip = ob.mergeTip;
-    if (mergeTip != null
-        && (branchTip == null || branchTip != mergeTip.getCurrentTip())) {
-      logDebug("Updating submodule subscriptions for branch {}", ob.name);
-      try {
-        subOp.updateSubmoduleSubscriptions(db, ob.name);
-      } catch (SubmoduleException e) {
-        logError("The submodule subscriptions were not updated according"
-            + "to the .gitmodules files", e);
-      }
-    }
-  }
-
   private void updateSuperProjects(SubmoduleOp subOp,
       Collection<Branch.NameKey> branches) {
     logDebug("Updating superprojects");
     try {
-      subOp.updateSuperProjects(db, branches);
+      subOp.updateSuperProjects(db, branches, submissionId);
+      logDebug("Updating superprojects done");
     } catch (SubmoduleException e) {
       logError("The gitlinks were not updated according to the "
           + "subscriptions", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 8296afa..1edfa16 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -39,11 +39,13 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.account.GroupBackend;
@@ -56,6 +58,7 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.util.StringUtils;
 
 import java.io.IOException;
@@ -125,6 +128,9 @@
   private static final String KEY_MERGE_CONTENT = "mergeContent";
   private static final String KEY_STATE = "state";
 
+  private static final String SUBSCRIBE_SECTION = "allowSuperproject";
+  private static final String SUBSCRIBE_REFS = "refs";
+
   private static final String DASHBOARD = "dashboard";
   private static final String KEY_DEFAULT = "default";
   private static final String KEY_LOCAL_DEFAULT = "local-default";
@@ -161,6 +167,7 @@
   private Map<String, NotifyConfig> notifySections;
   private Map<String, LabelType> labelSections;
   private ConfiguredMimeTypes mimeTypes;
+  private Map<Project.NameKey, SubscribeSection> subscribeSections;
   private List<CommentLinkInfo> commentLinkSections;
   private List<ValidationError> validationErrors;
   private ObjectId rulesId;
@@ -255,6 +262,21 @@
     return branchOrderSection;
   }
 
+  public Collection<SubscribeSection> getSubscribeSections(
+      Branch.NameKey branch) {
+    Collection<SubscribeSection> ret = new ArrayList<>();
+    for (SubscribeSection s : subscribeSections.values()) {
+      if (s.appliesTo(branch)) {
+        ret.add(s);
+      }
+    }
+    return ret;
+  }
+
+  public void addSubscribeSection(SubscribeSection s) {
+    subscribeSections.put(s.getProject(), s);
+  }
+
   public void remove(AccessSection section) {
     if (section != null) {
       accessSections.remove(section.getName());
@@ -440,6 +462,7 @@
     loadNotifySections(rc, groupsByName);
     loadLabelSections(rc);
     loadCommentLinkSections(rc);
+    loadSubscribeSections(rc);
     mimeTypes = new ConfiguredMimeTypes(projectName.get(), rc);
     loadPluginSections(rc);
     loadReceiveSection(rc);
@@ -771,6 +794,24 @@
     commentLinkSections = ImmutableList.copyOf(commentLinkSections);
   }
 
+  private void loadSubscribeSections(Config rc) throws ConfigInvalidException {
+    Set<String> subsections = rc.getSubsections(SUBSCRIBE_SECTION);
+    subscribeSections = new HashMap<>();
+    try {
+      for (String projectName : subsections) {
+        Project.NameKey p = new Project.NameKey(projectName);
+        SubscribeSection ss = new SubscribeSection(p);
+        for (String s : rc.getStringList(SUBSCRIBE_SECTION,
+            projectName, SUBSCRIBE_REFS)) {
+          ss.addRefSpec(s);
+        }
+        subscribeSections.put(p, ss);
+      }
+    } catch (IllegalArgumentException e) {
+      throw new ConfigInvalidException(e.getMessage());
+    }
+  }
+
   private void loadReceiveSection(Config rc) {
     checkReceivedObjects = rc.getBoolean(RECEIVE, KEY_CHECK_RECEIVED_OBJECTS, true);
     maxObjectSizeLimit = rc.getLong(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, 0);
@@ -865,6 +906,7 @@
     savePluginSections(rc, keepGroups);
     groupList.retainUUIDs(keepGroups);
     saveLabelSections(rc);
+    saveSubscribeSections(rc);
 
     saveConfig(PROJECT_CONFIG, rc);
     saveGroupList();
@@ -1147,6 +1189,15 @@
     saveUTF8(GroupList.FILE_NAME, groupList.asText());
   }
 
+  private void saveSubscribeSections(Config rc) {
+    for (Project.NameKey p : subscribeSections.keySet()) {
+      SubscribeSection s = subscribeSections.get(p);
+      for (RefSpec r : s.getRefSpecs()) {
+        rc.setString(SUBSCRIBE_SECTION, p.get(), SUBSCRIBE_REFS, r.toString());
+      }
+    }
+  }
+
   private <E extends Enum<?>> E getEnum(Config rc, String section,
       String subsection, String name, E defaultValue) {
     try {
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 775ecae..fd4400d 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
@@ -79,7 +79,6 @@
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -127,7 +126,6 @@
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -643,17 +641,6 @@
                 break;
 
               case DELETE:
-                ResultSet<SubmoduleSubscription> submoduleSubscriptions = null;
-                Branch.NameKey projRef = new Branch.NameKey(project.getNameKey(),
-                    c.getRefName());
-                try {
-                  submoduleSubscriptions =
-                      db.submoduleSubscriptions().bySuperProject(projRef);
-                  db.submoduleSubscriptions().delete(submoduleSubscriptions);
-                } catch (OrmException e) {
-                  log.error("Cannot delete submodule subscription(s) of branch "
-                      + projRef + ": " + submoduleSubscriptions, e);
-                }
                 break;
             }
           }
@@ -681,11 +668,9 @@
     // Update superproject gitlinks if required.
     SubmoduleOp op = subOpProvider.get();
     try {
-       op.updateSubmoduleSubscriptions(db, branches);
-       op.updateSuperProjects(db, branches);
+      op.updateSuperProjects(db, branches, "receiveID");
     } catch (SubmoduleException e) {
-      log.error("Can't update submodule subscriptions "
-          + "or update the superprojects", e);
+      log.error("Can't update the superprojects", e);
     }
 
     closeProgress.end();
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 ba9795a..432a6cd 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
@@ -409,11 +409,11 @@
       return;
     }
 
-    /** For labels that are not set in this operation, show the "current" value
+    /* For labels that are not set in this operation, show the "current" value
      * of 0, and no oldValue as the value was not modified by this operation.
      * For labels that are set in this operation, the value was modified, so
      * show a transition from an oldValue of 0 to the new value.
-     **/
+     */
     ChangeControl changeControl = changeControlFactory.controlFor(
         ctx.getDb(), change, ctx.getUser());
     List<LabelType> labels = changeControl.getLabelTypes().getLabelTypes();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 5a91206..bd8c156 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -19,18 +19,18 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.SubscribeSection;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.util.SubmoduleSectionParser;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -38,10 +38,8 @@
 import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
 import org.eclipse.jgit.dircache.DirCacheEntry;
-import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.BlobBasedConfig;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -54,164 +52,141 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.transport.RefSpec;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
+import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
 public class SubmoduleOp {
   private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
-  private static final String GIT_MODULES = ".gitmodules";
 
-  private final Provider<String> urlProvider;
+  private final GitModules.Factory gitmodulesFactory;
   private final PersonIdent myIdent;
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated gitRefUpdated;
+  private final ProjectCache projectCache;
   private final Set<Branch.NameKey> updatedSubscribers;
   private final Account account;
   private final ChangeHooks changeHooks;
-  private final SubmoduleSectionParser.Factory subSecParserFactory;
   private final boolean verboseSuperProject;
+  private final boolean enableSuperProjectSubscriptions;
+  private String updateId;
 
   @Inject
   public SubmoduleOp(
-      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      GitModules.Factory gitmodulesFactory,
       @GerritPersonIdent PersonIdent myIdent,
       @GerritServerConfig Config cfg,
       GitRepositoryManager repoManager,
       GitReferenceUpdated gitRefUpdated,
+      ProjectCache projectCache,
       @Nullable Account account,
-      ChangeHooks changeHooks,
-      SubmoduleSectionParser.Factory subSecParserFactory) {
-    this.urlProvider = urlProvider;
+      ChangeHooks changeHooks) {
+    this.gitmodulesFactory = gitmodulesFactory;
     this.myIdent = myIdent;
     this.repoManager = repoManager;
     this.gitRefUpdated = gitRefUpdated;
+    this.projectCache = projectCache;
     this.account = account;
     this.changeHooks = changeHooks;
-    this.subSecParserFactory = subSecParserFactory;
     this.verboseSuperProject = cfg.getBoolean("submodule",
         "verboseSuperprojectUpdate", true);
-
+    this.enableSuperProjectSubscriptions = cfg.getBoolean("submodule",
+        "enableSuperProjectSubscriptions", true);
     updatedSubscribers = new HashSet<>();
+
   }
 
-  void updateSubmoduleSubscriptions(ReviewDb db, Set<Branch.NameKey> branches)
-      throws SubmoduleException {
-    for (Branch.NameKey branch : branches) {
-      updateSubmoduleSubscriptions(db, branch);
-    }
-  }
-
-  void updateSubmoduleSubscriptions(ReviewDb db, Branch.NameKey destBranch)
-      throws SubmoduleException {
-    if (urlProvider.get() == null) {
-      logAndThrowSubmoduleException("Cannot establish canonical web url used "
-          + "to access gerrit. It should be provided in gerrit.config file.");
-    }
-    try (Repository repo = repoManager.openRepository(
-            destBranch.getParentKey());
-        RevWalk rw = new RevWalk(repo)) {
-
-      ObjectId id = repo.resolve(destBranch.get());
-      if (id == null) {
-        logAndThrowSubmoduleException(
-            "Cannot resolve submodule destination branch " + destBranch);
-      }
-      RevCommit commit = rw.parseCommit(id);
-
-      Set<SubmoduleSubscription> oldSubscriptions =
-          Sets.newHashSet(db.submoduleSubscriptions()
-              .bySuperProject(destBranch));
-
-      Set<SubmoduleSubscription> newSubscriptions;
-      TreeWalk tw = TreeWalk.forPath(repo, GIT_MODULES, commit.getTree());
-      if (tw != null
-          && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) ||
-              FileMode.EXECUTABLE_FILE.equals(tw.getRawMode(0)))) {
-        BlobBasedConfig bbc =
-            new BlobBasedConfig(null, repo, commit, GIT_MODULES);
-
-        String thisServer = new URI(urlProvider.get()).getHost();
-
-        newSubscriptions = subSecParserFactory.create(bbc, thisServer,
-            destBranch).parseAllSections();
-      } else {
-        newSubscriptions = Collections.emptySet();
-      }
-
-      Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
-      for (SubmoduleSubscription s : newSubscriptions) {
-        if (oldSubscriptions.contains(s)) {
-          alreadySubscribeds.add(s);
+  public Collection<Branch.NameKey> getDestinationBranches(Branch.NameKey src,
+      SubscribeSection s) throws IOException {
+    Collection<Branch.NameKey> ret = new ArrayList<>();
+    logDebug("Inspecting SubscribeSection " + s);
+    for (RefSpec r : s.getRefSpecs()) {
+      logDebug("Inspecting ref " + r);
+      if (r.matchSource(src.get())) {
+        if (r.getDestination() == null) {
+          // no need to care for wildcard, as we matched already
+          try (Repository repo = repoManager.openRepository(s.getProject())) {
+            for (Ref ref : repo.getRefDatabase().getRefs(
+                RefNames.REFS_HEADS).values()) {
+              ret.add(new Branch.NameKey(s.getProject(), ref.getName()));
+            }
+          }
+        } else if (r.isWildcard()) {
+          // refs/heads/*:refs/heads/*
+          ret.add(new Branch.NameKey(s.getProject(),
+              r.expandFromSource(src.get()).getDestination()));
+        } else {
+          // e.g. refs/heads/master:refs/heads/stable
+          ret.add(new Branch.NameKey(s.getProject(), r.getDestination()));
         }
       }
-
-      oldSubscriptions.removeAll(newSubscriptions);
-      newSubscriptions.removeAll(alreadySubscribeds);
-
-      if (!oldSubscriptions.isEmpty()) {
-        db.submoduleSubscriptions().delete(oldSubscriptions);
-      }
-      if (!newSubscriptions.isEmpty()) {
-        db.submoduleSubscriptions().insert(newSubscriptions);
-      }
-
-    } catch (OrmException e) {
-      logAndThrowSubmoduleException(
-          "Database problem at update of subscriptions table from "
-              + GIT_MODULES + " file.", e);
-    } catch (ConfigInvalidException e) {
-      logAndThrowSubmoduleException(
-          "Problem at update of subscriptions table: " + GIT_MODULES
-              + " config file is invalid.", e);
-    } catch (IOException e) {
-      logAndThrowSubmoduleException(
-          "Problem at update of subscriptions table from " + GIT_MODULES + ".",
-          e);
-    } catch (URISyntaxException e) {
-      logAndThrowSubmoduleException(
-          "Incorrect gerrit canonical web url provided in gerrit.config file.",
-          e);
     }
+    logDebug("Returning possible branches: " + ret +
+        "for project " + s.getProject());
+    return ret;
+  }
+
+  public Collection<SubmoduleSubscription>
+      superProjectSubscriptionsForSubmoduleBranch(
+      Branch.NameKey branch) throws IOException {
+    logDebug("Calculating possible superprojects for " + branch);
+    Collection<SubmoduleSubscription> ret = new ArrayList<>();
+    Project.NameKey project = branch.getParentKey();
+    ProjectConfig cfg = projectCache.get(project).getConfig();
+    for (SubscribeSection s : cfg.getSubscribeSections(branch)) {
+      Collection<Branch.NameKey> branches = getDestinationBranches(branch, s);
+      for (Branch.NameKey targetBranch : branches) {
+        GitModules m = gitmodulesFactory.create(targetBranch, updateId);
+        m.load();
+        ret.addAll(m.subscribedTo(branch));
+      }
+    }
+    logDebug("Calculated superprojects for " + branch + " are "+ ret);
+    return ret;
   }
 
   protected void updateSuperProjects(ReviewDb db,
-      Collection<Branch.NameKey> updatedBranches) throws SubmoduleException {
-    try {
-      // These (repo/branch) will be updated later with all the given
-      // individual submodule subscriptions
-      Multimap<Branch.NameKey, SubmoduleSubscription> targets =
-          HashMultimap.create();
+      Collection<Branch.NameKey> updatedBranches, String updateId)
+          throws SubmoduleException {
+    if (!enableSuperProjectSubscriptions) {
+      logDebug("Updating superprojects disabled");
+      return;
+    }
+    this.updateId = updateId;
+    logDebug("Updating superprojects");
+    // These (repo/branch) will be updated later with all the given
+    // individual submodule subscriptions
+    Multimap<Branch.NameKey, SubmoduleSubscription> targets =
+        HashMultimap.create();
 
+    try {
       for (Branch.NameKey updatedBranch : updatedBranches) {
-        for (SubmoduleSubscription sub : db.submoduleSubscriptions()
-            .bySubmodule(updatedBranch)) {
+        for (SubmoduleSubscription sub :
+          superProjectSubscriptionsForSubmoduleBranch(updatedBranch)) {
           targets.put(sub.getSuperProject(), sub);
         }
       }
-      updatedSubscribers.addAll(updatedBranches);
-      // Update subscribers.
-      for (Branch.NameKey dest : targets.keySet()) {
-        try {
-          if (!updatedSubscribers.add(dest)) {
-            log.error("Possible circular subscription involving " + dest);
-          } else {
-            updateGitlinks(db, dest, targets.get(dest));
-          }
-        } catch (SubmoduleException e) {
-          log.warn("Cannot update gitlinks for " + dest, e);
+    } catch (IOException e) {
+      throw new SubmoduleException("Could not calculate all superprojects");
+    }
+    updatedSubscribers.addAll(updatedBranches);
+    // Update subscribers.
+    for (Branch.NameKey dest : targets.keySet()) {
+      try {
+        if (!updatedSubscribers.add(dest)) {
+          log.error("Possible circular subscription involving " + dest);
+        } else {
+          updateGitlinks(db, dest, targets.get(dest));
         }
+      } catch (SubmoduleException e) {
+        log.warn("Cannot update gitlinks for " + dest, e);
       }
-    } catch (OrmException e) {
-      logAndThrowSubmoduleException("Cannot read subscription records", e);
     }
   }
 
@@ -292,7 +267,7 @@
                 msgbuf.append(c.getFullMessage() + "\n\n");
               }
             } catch (IOException e) {
-              logAndThrowSubmoduleException("Could not perform a revwalk to "
+              throw new SubmoduleException("Could not perform a revwalk to "
                   + "create superproject commit message", e);
             }
           }
@@ -347,7 +322,7 @@
           throw new IOException(rfu.getResult().name());
       }
       // Recursive call: update subscribers of the subscriber
-      updateSuperProjects(db, Sets.newHashSet(subscriber));
+      updateSuperProjects(db, Sets.newHashSet(subscriber), updateId);
     } catch (IOException e) {
       throw new SubmoduleException("Cannot update gitlinks for "
           + subscriber.get(), e);
@@ -367,15 +342,9 @@
     }
   }
 
-  private static void logAndThrowSubmoduleException(final String errorMsg,
-      final Exception e) throws SubmoduleException {
-    log.error(errorMsg, e);
-    throw new SubmoduleException(errorMsg, e);
-  }
-
-  private static void logAndThrowSubmoduleException(final String errorMsg)
-      throws SubmoduleException {
-    log.error(errorMsg);
-    throw new SubmoduleException(errorMsg);
+  private void logDebug(String msg, Object... args) {
+    if (log.isDebugEnabled()) {
+      log.debug("[" + updateId + "]" + msg, args);
+    }
   }
 }
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 b32d668..8f15c44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -361,7 +361,7 @@
     PatchSet.Id psId = update.getPatchSetId();
     ctx.getDb().patchSetApprovals().upsert(
         convertPatchSet(normalized.getNormalized(), psId));
-    ctx.getDb().patchSetApprovals().update(
+    ctx.getDb().patchSetApprovals().upsert(
         zero(convertPatchSet(normalized.deleted(), psId)));
     for (PatchSetApproval psa : normalized.updated()) {
       update.putApprovalFor(psa.getAccountId(), psa.getLabel(), psa.getValue());
@@ -370,7 +370,7 @@
       update.removeApprovalFor(psa.getAccountId(), psa.getLabel());
     }
 
-    // TODO(dborowitz): Don't use a label in notedb; just check when status
+    // TODO(dborowitz): Don't use a label in NoteDb; just check when status
     // change happened.
     for (PatchSetApproval psa : normalized.unchanged()) {
       if (includeUnchanged || psa.isLegacySubmit()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
index 46a2b7d..2aa2bdb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
@@ -14,12 +14,18 @@
 
 package com.google.gerrit.server.index;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 import com.google.common.base.Stopwatch;
 
+import org.eclipse.jgit.util.io.NullOutputStream;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.util.concurrent.TimeUnit;
 
-public interface SiteIndexer<K, V, I extends Index<K, V>> {
-  public class Result {
+public abstract class SiteIndexer<K, V, I extends Index<K, V>> {
+  public static class Result {
     private final long elapsedNanos;
     private final boolean success;
     private final int done;
@@ -49,5 +55,22 @@
     }
   }
 
-  Result indexAll(I index);
+  protected int totalWork = -1;
+  protected OutputStream progressOut = NullOutputStream.INSTANCE;
+  protected PrintWriter verboseWriter =
+      new PrintWriter(NullOutputStream.INSTANCE);
+
+  public void setTotalWork(int num) {
+    totalWork = num;
+  }
+
+  public void setProgressOut(OutputStream out) {
+    progressOut = checkNotNull(out);
+  }
+
+  public void setVerboseOut(OutputStream out) {
+    verboseWriter = new PrintWriter(checkNotNull(out));
+  }
+
+  public abstract Result indexAll(I index);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 4fe589c..a38b1e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.index.change;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
@@ -56,18 +55,17 @@
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.merge.ThreeWayMergeStrategy;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
-import org.eclipse.jgit.util.io.NullOutputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.Collection;
 import java.util.Collections;
@@ -80,7 +78,7 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 public class AllChangesIndexer
-    implements SiteIndexer<Change.Id, ChangeData, ChangeIndex> {
+    extends SiteIndexer<Change.Id, ChangeData, ChangeIndex> {
   private static final Logger log =
       LoggerFactory.getLogger(AllChangesIndexer.class);
 
@@ -93,11 +91,6 @@
   private final ProjectCache projectCache;
   private final ThreeWayMergeStrategy mergeStrategy;
 
-  private int totalWork = -1;
-  private OutputStream progressOut = NullOutputStream.INSTANCE;
-  private PrintWriter verboseWriter =
-      new PrintWriter(NullOutputStream.INSTANCE);
-
   @Inject
   AllChangesIndexer(SchemaFactory<ReviewDb> schemaFactory,
       ChangeData.Factory changeDataFactory,
@@ -117,24 +110,26 @@
     this.mergeStrategy = MergeUtil.getMergeStrategy(config);
   }
 
-  public AllChangesIndexer setTotalWork(int num) {
-    totalWork = num;
-    return this;
-  }
-
-  public AllChangesIndexer setProgressOut(OutputStream out) {
-    progressOut = checkNotNull(out);
-    return this;
-  }
-
-  public AllChangesIndexer setVerboseOut(OutputStream out) {
-    verboseWriter = new PrintWriter(checkNotNull(out));
-    return this;
-  }
-
   @Override
   public Result indexAll(ChangeIndex index) {
-    return indexAll(index, projectCache.all());
+    ProgressMonitor pm = new TextProgressMonitor();
+    pm.beginTask("Collecting projects", ProgressMonitor.UNKNOWN);
+    Set<Project.NameKey> projects = Sets.newTreeSet();
+    int changeCount = 0;
+    Stopwatch sw = Stopwatch.createStarted();
+    for (Project.NameKey project : projectCache.all()) {
+      try (Repository repo = repoManager.openRepository(project)) {
+        changeCount += ChangeNotes.Factory.scan(repo).size();
+      } catch (IOException e) {
+        log.error("Error collecting projects", e);
+        return new Result(sw, false, 0, 0);
+      }
+      projects.add(project);
+      pm.update(1);
+    }
+    pm.endTask();
+    setTotalWork(changeCount);
+    return indexAll(index, projects);
   }
 
   public SiteIndexer.Result indexAll(ChangeIndex index,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index bb6ca45..dfc41ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -59,7 +59,7 @@
 
   public static QueryOptions createOptions(IndexConfig config, int start,
       int limit, Set<String> fields) {
-    // Always include project since it is needed to load the change from notedb.
+    // Always include project since it is needed to load the change from NoteDb.
     if (!fields.contains(CHANGE.getName())
         && !fields.contains(PROJECT.getName())) {
       fields = new HashSet<>(fields);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java
new file mode 100644
index 0000000..05937d5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java
@@ -0,0 +1,54 @@
+// 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.mail;
+
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/** Send notice about a vote that was removed from a change. */
+public class DeleteVoteSender extends ReplyToChangeSender {
+  public static interface Factory extends
+      ReplyToChangeSender.Factory<DeleteVoteSender> {
+    @Override
+    DeleteVoteSender create(Project.NameKey project, Change.Id change);
+  }
+
+  @Inject
+  protected DeleteVoteSender(EmailArguments ea,
+      @Assisted Project.NameKey project,
+      @Assisted Change.Id id)
+      throws OrmException {
+    super(ea, "deleteVote", newChangeData(ea, project, id));
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+
+    ccAllApprovals();
+    bccStarredBy();
+    includeWatchers(NotifyType.ALL_COMMENTS);
+  }
+
+  @Override
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("DeleteVote.vm"));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
index a125517..a9240cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
@@ -21,7 +21,8 @@
   protected void configure() {
     factory(AbandonedSender.Factory.class);
     factory(CommentSender.Factory.class);
-    factory(RevertedSender.Factory.class);
+    factory(DeleteVoteSender.Factory.class);
     factory(RestoredSender.Factory.class);
+    factory(RevertedSender.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index d29710e..0442e23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.mail;
 
+import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS;
+import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
@@ -106,10 +108,10 @@
     if (shouldSendMessage()) {
       if (fromId != null) {
         final Account fromUser = args.accountCache.get(fromId).getAccount();
-        EmailStrategy strategy =
-            fromUser.getGeneralPreferencesInfo().getEmailStrategy();
+        GeneralPreferencesInfo senderPrefs = fromUser.getGeneralPreferencesInfo();
 
-        if (strategy == EmailStrategy.CC_ON_OWN_COMMENTS) {
+        if (senderPrefs != null
+            && senderPrefs.getEmailStrategy() == CC_ON_OWN_COMMENTS) {
           // If we are impersonating a user, make sure they receive a CC of
           // this message so they can always review and audit what we sent
           // on their behalf to others.
@@ -126,11 +128,10 @@
         // his email notifications then drop him from recipients' list
         for (Account.Id id : rcptTo) {
           Account thisUser = args.accountCache.get(id).getAccount();
-          if (thisUser.getGeneralPreferencesInfo().getEmailStrategy()
-                  == EmailStrategy.DISABLED) {
+          GeneralPreferencesInfo prefs = thisUser.getGeneralPreferencesInfo();
+          if (prefs == null || prefs.getEmailStrategy() == DISABLED) {
             removeUser(thisUser);
           }
-
           if (smtpRcptTo.isEmpty()) {
             return;
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index c22d4f8..621f6e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -14,31 +14,60 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.VersionedMetaData;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
 
 /** View of contents at a single ref related to some change. **/
-public abstract class AbstractChangeNotes<T> extends VersionedMetaData {
-  protected final GitRepositoryManager repoManager;
-  protected final NotesMigration migration;
+public abstract class AbstractChangeNotes<T> {
+  @VisibleForTesting
+  @Singleton
+  public static class Args {
+    final GitRepositoryManager repoManager;
+    final NotesMigration migration;
+    final AllUsersName allUsers;
+    final ChangeNoteUtil noteUtil;
+    final NoteDbMetrics metrics;
+
+    @Inject
+    Args(
+        GitRepositoryManager repoManager,
+        NotesMigration migration,
+        AllUsersName allUsers,
+        ChangeNoteUtil noteUtil,
+        NoteDbMetrics metrics) {
+      this.repoManager = repoManager;
+      this.migration = migration;
+      this.allUsers = allUsers;
+      this.noteUtil = noteUtil;
+      this.metrics = metrics;
+    }
+  }
+
+  protected final Args args;
   private final Change.Id changeId;
 
+  private ObjectId revision;
   private boolean loaded;
 
-  AbstractChangeNotes(GitRepositoryManager repoManager,
-      NotesMigration migration, Change.Id changeId) {
-    this.repoManager = repoManager;
-    this.migration = migration;
+  AbstractChangeNotes(Args args, Change.Id changeId) {
+    this.args = args;
     this.changeId = changeId;
   }
 
@@ -46,16 +75,27 @@
     return changeId;
   }
 
+  /** @return revision of the metadata that was loaded. */
+  public ObjectId getRevision() {
+    return revision;
+  }
+
   public T load() throws OrmException {
     if (loaded) {
       return self();
     }
-    if (!migration.enabled() || changeId == null) {
+    if (!args.migration.enabled() || changeId == null) {
       loadDefaults();
       return self();
     }
-    try (Repository repo = repoManager.openMetadataRepository(getProjectName())) {
-      load(repo);
+    try (Timer1.Context timer = args.metrics.readLatency.start(CHANGES);
+        Repository repo =
+            args.repoManager.openMetadataRepository(getProjectName());
+        RevWalk walk = new RevWalk(repo)) {
+      Ref ref = repo.getRefDatabase().exactRef(getRefName());
+      ObjectId id = ref != null ? ref.getObjectId() : null;
+      revision = id != null ? walk.parseCommit(id).copy() : null;
+      onLoad(walk);
       loaded = true;
     } catch (ConfigInvalidException | IOException e) {
       throw new OrmException(e);
@@ -71,10 +111,11 @@
   public ObjectId loadRevision() throws OrmException {
     if (loaded) {
       return getRevision();
-    } else if (!migration.enabled()) {
+    } else if (!args.migration.enabled()) {
       return null;
     }
-    try (Repository repo = repoManager.openMetadataRepository(getProjectName())) {
+    try (Repository repo =
+        args.repoManager.openMetadataRepository(getProjectName())) {
       Ref ref = repo.getRefDatabase().exactRef(getRefName());
       return ref != null ? ref.getObjectId() : null;
     } catch (IOException e) {
@@ -82,7 +123,7 @@
     }
   }
 
-  /** Load default values for any instance variables when notedb is disabled. */
+  /** Load default values for any instance variables when NoteDb is disabled. */
   protected abstract void loadDefaults();
 
   /**
@@ -91,6 +132,13 @@
    */
   public abstract Project.NameKey getProjectName();
 
+  /** @return name of the reference storing this configuration. */
+  protected abstract String getRefName();
+
+  /** Set up the metadata, parsing any state from the loaded revision. */
+  protected abstract void onLoad(RevWalk walk)
+      throws IOException, ConfigInvalidException;
+
   @SuppressWarnings("unchecked")
   protected final T self() {
     return (T) this;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 9669a6b..f2d3a09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -183,6 +183,10 @@
     return result;
   }
 
+  public boolean allowWriteToNewRef() {
+    return true;
+  }
+
   private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
     return ins.insert(Constants.OBJ_TREE, new byte[] {});
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDelete.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDelete.java
index fc05888..24391ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDelete.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDelete.java
@@ -42,7 +42,7 @@
     ru.setExpectedOldObjectId(notes.load().getRevision());
     ru.setNewObjectId(ObjectId.zeroId());
     ru.setForceUpdate(true);
-    ru.setRefLogMessage("Delete change from notedb", false);
+    ru.setRefLogMessage("Delete change from NoteDb", false);
     RefUpdate.Result result = ru.delete();
     switch (result) {
       case FAST_FORWARD:
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 2938d6b..1a5beb3 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
@@ -53,8 +53,6 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -66,7 +64,6 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -114,27 +111,18 @@
   public static class Factory {
     private static final Logger log = LoggerFactory.getLogger(Factory.class);
 
-    private final GitRepositoryManager repoManager;
-    private final NotesMigration migration;
-    private final AllUsersName allUsers;
+    private final Args args;
     private final Provider<InternalChangeQuery> queryProvider;
     private final ProjectCache projectCache;
-    private final ChangeNoteUtil noteUtil;
 
     @VisibleForTesting
     @Inject
-    public Factory(GitRepositoryManager repoManager,
-        NotesMigration migration,
-        AllUsersName allUsers,
+    public Factory(Args args,
         Provider<InternalChangeQuery> queryProvider,
-        ProjectCache projectCache,
-        ChangeNoteUtil noteUtil) {
-      this.repoManager = repoManager;
-      this.migration = migration;
-      this.allUsers = allUsers;
+        ProjectCache projectCache) {
+      this.args = args;
       this.queryProvider = queryProvider;
       this.projectCache = projectCache;
-      this.noteUtil = noteUtil;
     }
 
     public ChangeNotes createChecked(ReviewDb db, Change c)
@@ -179,8 +167,7 @@
           project, changeId, change.getProject());
       // TODO: Throw NoSuchChangeException when the change is not found in the
       // database
-      return new ChangeNotes(repoManager, migration, allUsers, noteUtil,
-          project, change).load();
+      return new ChangeNotes(args, project, change).load();
     }
 
     /**
@@ -192,23 +179,20 @@
      * @return change notes
      */
     public ChangeNotes createFromIndexedChange(Change change) {
-      return new ChangeNotes(repoManager, migration, allUsers, noteUtil,
-          change.getProject(), change);
+      return new ChangeNotes(args, change.getProject(), change);
     }
 
     public ChangeNotes createForNew(Change change) throws OrmException {
-      return new ChangeNotes(repoManager, migration, allUsers, noteUtil,
-          change.getProject(), change).load();
+      return new ChangeNotes(args, change.getProject(), change).load();
     }
 
     // TODO(dborowitz): Remove when deleting index schemas <27.
-    public ChangeNotes createFromIdOnlyWhenNotedbDisabled(
+    public ChangeNotes createFromIdOnlyWhenNoteDbDisabled(
         ReviewDb db, Change.Id changeId) throws OrmException {
-    checkState(!migration.readChanges(), "do not call"
-        + " createFromIdOnlyWhenNotedbDisabled when notedb is enabled");
+    checkState(!args.migration.readChanges(), "do not call"
+        + " createFromIdOnlyWhenNoteDbDisabled when NoteDb is enabled");
       Change change = unwrap(db).changes().get(changeId);
-      return new ChangeNotes(repoManager, migration, allUsers, noteUtil,
-          change.getProject(), change).load();
+      return new ChangeNotes(args, change.getProject(), change).load();
     }
 
     // TODO(ekempin): Remove when database backend is deleted
@@ -216,12 +200,11 @@
      * Instantiate ChangeNotes for a change that has been loaded by a batch read
      * from the database.
      */
-    private ChangeNotes createFromChangeOnlyWhenNotedbDisabled(Change change)
+    private ChangeNotes createFromChangeOnlyWhenNoteDbDisabled(Change change)
         throws OrmException {
-      checkState(!migration.readChanges(), "do not call"
-          + " createFromChangeWhenNotedbDisabled when notedb is enabled");
-      return new ChangeNotes(repoManager, migration, allUsers, noteUtil,
-          change.getProject(), change).load();
+      checkState(!args.migration.readChanges(), "do not call"
+          + " createFromChangeWhenNoteDbDisabled when NoteDb is enabled");
+      return new ChangeNotes(args, change.getProject(), change).load();
     }
 
     public CheckedFuture<ChangeNotes, OrmException> createAsync(
@@ -240,8 +223,7 @@
                           "passed project %s when creating ChangeNotes for %s,"
                               + " but actual project is %s",
                           project, changeId, change.getProject());
-                      return new ChangeNotes(repoManager, migration, allUsers,
-                          noteUtil, project, change).load();
+                      return new ChangeNotes(args, project, change).load();
                     }
                   });
                 }
@@ -259,7 +241,7 @@
     public List<ChangeNotes> create(ReviewDb db,
         Collection<Change.Id> changeIds) throws OrmException {
       List<ChangeNotes> notes = new ArrayList<>();
-      if (migration.enabled()) {
+      if (args.migration.enabled()) {
         for (Change.Id changeId : changeIds) {
           try {
             notes.add(createChecked(changeId));
@@ -271,7 +253,7 @@
       }
 
       for (Change c : unwrap(db).changes().get(changeIds)) {
-        notes.add(createFromChangeOnlyWhenNotedbDisabled(c));
+        notes.add(createFromChangeOnlyWhenNoteDbDisabled(c));
       }
       return notes;
     }
@@ -280,7 +262,7 @@
         Collection<Change.Id> changeIds, Predicate<ChangeNotes> predicate)
             throws OrmException {
       List<ChangeNotes> notes = new ArrayList<>();
-      if (migration.enabled()) {
+      if (args.migration.enabled()) {
         for (Change.Id cid : changeIds) {
           ChangeNotes cn = create(db, project, cid);
           if (cn.getChange() != null && predicate.apply(cn)) {
@@ -292,7 +274,7 @@
 
       for (Change c : unwrap(db).changes().get(changeIds)) {
         if (c != null && project.equals(c.getDest().getParentKey())) {
-          ChangeNotes cn = createFromChangeOnlyWhenNotedbDisabled(c);
+          ChangeNotes cn = createFromChangeOnlyWhenNoteDbDisabled(c);
           if (predicate.apply(cn)) {
             notes.add(cn);
           }
@@ -304,10 +286,10 @@
     public ListMultimap<Project.NameKey, ChangeNotes> create(ReviewDb db,
         Predicate<ChangeNotes> predicate) throws IOException, OrmException {
       ListMultimap<Project.NameKey, ChangeNotes> m = ArrayListMultimap.create();
-      if (migration.readChanges()) {
+      if (args.migration.readChanges()) {
         for (Project.NameKey project : projectCache.all()) {
-          try (Repository repo = repoManager.openRepository(project)) {
-            List<ChangeNotes> changes = scanNotedb(repo, db, project);
+          try (Repository repo = args.repoManager.openRepository(project)) {
+            List<ChangeNotes> changes = scanNoteDb(repo, db, project);
             for (ChangeNotes cn : changes) {
               if (predicate.apply(cn)) {
                 m.put(project, cn);
@@ -317,7 +299,7 @@
         }
       } else {
         for (Change change : unwrap(db).changes().all()) {
-          ChangeNotes notes = createFromChangeOnlyWhenNotedbDisabled(change);
+          ChangeNotes notes = createFromChangeOnlyWhenNoteDbDisabled(change);
           if (predicate.apply(notes)) {
             m.put(change.getProject(), notes);
           }
@@ -328,11 +310,11 @@
 
     public List<ChangeNotes> scan(Repository repo, ReviewDb db,
         Project.NameKey project) throws OrmException, IOException {
-      if (!migration.readChanges()) {
+      if (!args.migration.readChanges()) {
         return scanDb(repo, db);
       }
 
-      return scanNotedb(repo, db, project);
+      return scanNoteDb(repo, db, project);
     }
 
     private List<ChangeNotes> scanDb(Repository repo, ReviewDb db)
@@ -343,13 +325,13 @@
       // but still >1.
       for (List<Change.Id> batch : Iterables.partition(ids, 30)) {
         for (Change change : unwrap(db).changes().get(batch)) {
-          notes.add(createFromChangeOnlyWhenNotedbDisabled(change));
+          notes.add(createFromChangeOnlyWhenNoteDbDisabled(change));
         }
       }
       return notes;
     }
 
-    private List<ChangeNotes> scanNotedb(Repository repo, ReviewDb db,
+    private List<ChangeNotes> scanNoteDb(Repository repo, ReviewDb db,
         Project.NameKey project) throws OrmException, IOException {
       Set<Change.Id> ids = scan(repo);
       List<ChangeNotes> changeNotes = new ArrayList<>(ids.size());
@@ -380,7 +362,6 @@
     }
   }
 
-  private final ChangeNoteUtil noteUtil;
   private final Project.NameKey project;
   private final Change change;
   private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
@@ -397,16 +378,11 @@
   // notes easier.
   RevisionNoteMap revisionNoteMap;
 
-  private final AllUsersName allUsers;
   private DraftCommentNotes draftCommentNotes;
 
   @VisibleForTesting
-  public ChangeNotes(GitRepositoryManager repoManager, NotesMigration migration,
-      AllUsersName allUsers, ChangeNoteUtil noteUtil,
-      Project.NameKey project, Change change) {
-    super(repoManager, migration, change != null ? change.getId() : null);
-    this.allUsers = allUsers;
-    this.noteUtil = noteUtil;
+  public ChangeNotes(Args args, Project.NameKey project, Change change) {
+    super(args, change != null ? change.getId() : null);
     this.project = project;
     this.change = change != null ? new Change(change) : null;
   }
@@ -503,8 +479,7 @@
       throws OrmException {
     if (draftCommentNotes == null ||
         !author.equals(draftCommentNotes.getAuthor())) {
-      draftCommentNotes = new DraftCommentNotes(repoManager, migration,
-          allUsers, noteUtil, getChangeId(), author);
+      draftCommentNotes = new DraftCommentNotes(args, getChangeId(), author);
       draftCommentNotes.load();
     }
   }
@@ -543,15 +518,16 @@
   }
 
   @Override
-  protected void onLoad() throws IOException, ConfigInvalidException {
+  protected void onLoad(RevWalk walk)
+      throws IOException, ConfigInvalidException {
     ObjectId rev = getRevision();
     if (rev == null) {
       loadDefaults();
       return;
     }
-    try (RevWalk walk = new RevWalk(reader);
-        ChangeNotesParser parser = new ChangeNotesParser(
-            project, change.getId(), rev, walk, repoManager, noteUtil)) {
+    try (ChangeNotesParser parser = new ChangeNotesParser(
+         project, change.getId(), rev, walk, args.repoManager, args.noteUtil,
+         args.metrics)) {
       parser.parseAll();
 
       if (parser.status != null) {
@@ -611,12 +587,6 @@
   }
 
   @Override
-  protected boolean onSave(CommitBuilder commit) {
-    throw new UnsupportedOperationException(
-        getClass().getSimpleName() + " is read-only");
-  }
-
-  @Override
   public Project.NameKey getProjectName() {
     return project;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 075ca36..7bf7960 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -26,6 +26,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
+import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 
 import com.google.common.base.Enums;
 import com.google.common.base.Function;
@@ -47,6 +48,7 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -115,6 +117,7 @@
   RevisionNoteMap revisionNoteMap;
 
   private final ChangeNoteUtil noteUtil;
+  private final NoteDbMetrics metrics;
   private final Change.Id id;
   private final ObjectId tip;
   private final RevWalk walk;
@@ -126,13 +129,14 @@
 
   ChangeNotesParser(Project.NameKey project, Change.Id changeId, ObjectId tip,
       RevWalk walk, GitRepositoryManager repoManager,
-      ChangeNoteUtil noteUtil)
+      ChangeNoteUtil noteUtil, NoteDbMetrics metrics)
       throws RepositoryNotFoundException, IOException {
     this.id = changeId;
     this.tip = tip;
     this.walk = walk;
     this.repo = repoManager.openMetadataRepository(project);
     this.noteUtil = noteUtil;
+    this.metrics = metrics;
     approvals = Maps.newHashMap();
     reviewers = Maps.newLinkedHashMap();
     allPastReviewers = Lists.newArrayList();
@@ -150,15 +154,21 @@
   }
 
   void parseAll() throws ConfigInvalidException, IOException {
+    // Don't include initial parse in timer, as this might do more I/O to page
+    // in the block containing most commits. Later reads are not guaranteed to
+    // avoid I/O, but often should.
     walk.markStart(walk.parseCommit(tip));
-    for (RevCommit commit : walk) {
-      parse(commit);
+
+    try (Timer1.Context timer = metrics.parseLatency.start(CHANGES)) {
+      for (RevCommit commit : walk) {
+        parse(commit);
+      }
+      parseNotes();
+      allPastReviewers.addAll(reviewers.keySet());
+      pruneReviewers();
+      updatePatchSetStates();
+      checkMandatoryFooters();
     }
-    parseNotes();
-    allPastReviewers.addAll(reviewers.keySet());
-    pruneReviewers();
-    updatePatchSetStates();
-    checkMandatoryFooters();
   }
 
   ImmutableListMultimap<PatchSet.Id, PatchSetApproval>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
index 16786b6..e1a9403 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -189,15 +189,15 @@
       events.add(new ApprovalEvent(psa, change.getCreatedOn()));
     }
 
-    Change notedbChange = new Change(null, null, null, null, null);
+    Change noteDbChange = new Change(null, null, null, null, null);
     for (ChangeMessage msg : db.changeMessages().byChange(changeId)) {
       events.add(
-          new ChangeMessageEvent(msg, notedbChange, change.getCreatedOn()));
+          new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn()));
     }
 
     Collections.sort(events, EVENT_ORDER);
 
-    events.add(new FinalUpdatesEvent(change, notedbChange));
+    events.add(new FinalUpdatesEvent(change, noteDbChange));
 
     EventList<Event> el = new EventList<>();
     for (Event e : events) {
@@ -235,6 +235,7 @@
     ChangeUpdate update = updateFactory.create(
         controlFactory.controlFor(db, change, events.getUser(db)),
         events.getWhen());
+    update.setAllowWriteToNewRef(true);
     update.setPatchSetId(events.getPatchSetId());
     for (Event e : events) {
       e.apply(update);
@@ -632,14 +633,14 @@
         Pattern.compile("^Restored(\n.*)*$");
 
     private final ChangeMessage message;
-    private final Change notedbChange;
+    private final Change noteDbChange;
 
-    ChangeMessageEvent(ChangeMessage message, Change notedbChange,
+    ChangeMessageEvent(ChangeMessage message, Change noteDbChange,
         Timestamp changeCreatedOn) {
       super(message.getPatchSetId(), message.getAuthor(),
           message.getWrittenOn(), changeCreatedOn);
       this.message = message;
-      this.notedbChange = notedbChange;
+      this.noteDbChange = noteDbChange;
     }
 
     @Override
@@ -661,7 +662,7 @@
       if (m.matches()) {
         String topic = m.group(1);
         update.setTopic(topic);
-        notedbChange.setTopic(topic);
+        noteDbChange.setTopic(topic);
         return;
       }
 
@@ -669,13 +670,13 @@
       if (m.matches()) {
         String topic = m.group(2);
         update.setTopic(topic);
-        notedbChange.setTopic(topic);
+        noteDbChange.setTopic(topic);
         return;
       }
 
       if (TOPIC_REMOVED_REGEXP.matcher(msg).matches()) {
         update.setTopic(null);
-        notedbChange.setTopic(null);
+        noteDbChange.setTopic(null);
       }
     }
 
@@ -683,26 +684,26 @@
       String msg = message.getMessage();
       if (STATUS_ABANDONED_REGEXP.matcher(msg).matches()) {
         update.setStatus(Change.Status.ABANDONED);
-        notedbChange.setStatus(Change.Status.ABANDONED);
+        noteDbChange.setStatus(Change.Status.ABANDONED);
         return;
       }
 
       if (STATUS_RESTORED_REGEXP.matcher(msg).matches()) {
         update.setStatus(Change.Status.NEW);
-        notedbChange.setStatus(Change.Status.NEW);
+        noteDbChange.setStatus(Change.Status.NEW);
       }
     }
   }
 
   private static class FinalUpdatesEvent extends Event {
     private final Change change;
-    private final Change notedbChange;
+    private final Change noteDbChange;
 
-    FinalUpdatesEvent(Change change, Change notedbChange) {
+    FinalUpdatesEvent(Change change, Change noteDbChange) {
       super(change.currentPatchSetId(), change.getOwner(),
           change.getLastUpdatedOn(), change.getCreatedOn());
       this.change = change;
-      this.notedbChange = notedbChange;
+      this.noteDbChange = noteDbChange;
     }
 
     @Override
@@ -713,10 +714,10 @@
     @SuppressWarnings("deprecation")
     @Override
     void apply(ChangeUpdate update) throws OrmException {
-      if (!Objects.equals(change.getTopic(), notedbChange.getTopic())) {
+      if (!Objects.equals(change.getTopic(), noteDbChange.getTopic())) {
         update.setTopic(change.getTopic());
       }
-      if (!Objects.equals(change.getStatus(), notedbChange.getStatus())) {
+      if (!Objects.equals(change.getStatus(), noteDbChange.getStatus())) {
         // TODO(dborowitz): Stamp approximate approvals at this time.
         update.fixStatus(change.getStatus());
       }
@@ -724,7 +725,7 @@
         update.setSubmissionId(change.getSubmissionId());
       }
       if (!update.isEmpty()) {
-        update.setSubjectForCommit("Final notedb migration updates");
+        update.setSubjectForCommit("Final NoteDb migration updates");
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index d308426..a7c06b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -121,6 +121,7 @@
   private PatchSetState psState;
   private Iterable<String> groups;
   private String pushCert;
+  private boolean isAllowWriteToNewtRef;
 
   private ChangeDraftUpdate draftUpdate;
 
@@ -306,7 +307,7 @@
   }
 
   private void verifyComment(PatchLineComment c) {
-    checkArgument(c.getRevId() != null);
+    checkArgument(c.getRevId() != null, "RevId required for comment: %s", c);
     checkArgument(c.getAuthor().equals(getUser().getAccountId()),
         "The author for the following comment does not match the author of"
         + " this ChangeDraftUpdate (%s): %s", getUser().getAccountId(), c);
@@ -606,6 +607,15 @@
     return draftUpdate;
   }
 
+  public void setAllowWriteToNewRef(boolean allow) {
+    isAllowWriteToNewtRef = allow;
+  }
+
+  @Override
+  public boolean allowWriteToNewRef() {
+    return isAllowWriteToNewtRef;
+  }
+
   private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
     return sb.append(footer.getName()).append(": ");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
index 17049c0..f1607e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.AbstractModule;
@@ -45,33 +46,25 @@
     }
   }
 
-  private static enum Table {
-    CHANGES;
-
-    private String key() {
-      return name().toLowerCase();
-    }
-  }
-
-  private static final String NOTEDB = "notedb";
+  private static final String NOTE_DB = "noteDb";
   private static final String READ = "read";
   private static final String WRITE = "write";
 
   private static void checkConfig(Config cfg) {
     Set<String> keys = new HashSet<>();
-    for (Table t : Table.values()) {
+    for (NoteDbTable t : NoteDbTable.values()) {
       keys.add(t.key());
     }
-    for (String t : cfg.getSubsections(NOTEDB)) {
+    for (String t : cfg.getSubsections(NOTE_DB)) {
       checkArgument(keys.contains(t.toLowerCase()),
-          "invalid notedb table: %s", t);
-      for (String key : cfg.getNames(NOTEDB, t)) {
+          "invalid NoteDb table: %s", t);
+      for (String key : cfg.getNames(NOTE_DB, t)) {
         String lk = key.toLowerCase();
         checkArgument(lk.equals(WRITE) || lk.equals(READ),
-            "invalid notedb key: %s.%s", t, key);
+            "invalid NoteDb key: %s.%s", t, key);
       }
-      boolean write = cfg.getBoolean(NOTEDB, t, WRITE, false);
-      boolean read = cfg.getBoolean(NOTEDB, t, READ, false);
+      boolean write = cfg.getBoolean(NOTE_DB, t, WRITE, false);
+      boolean read = cfg.getBoolean(NOTE_DB, t, READ, false);
       checkArgument(!(read && !write),
           "must have write enabled when read enabled: %s", t);
     }
@@ -79,9 +72,9 @@
 
   public static Config allEnabledConfig() {
     Config cfg = new Config();
-    for (Table t : Table.values()) {
-      cfg.setBoolean(NOTEDB, t.key(), WRITE, true);
-      cfg.setBoolean(NOTEDB, t.key(), READ, true);
+    for (NoteDbTable t : NoteDbTable.values()) {
+      cfg.setBoolean(NOTE_DB, t.key(), WRITE, true);
+      cfg.setBoolean(NOTE_DB, t.key(), READ, true);
     }
     return cfg;
   }
@@ -92,8 +85,8 @@
   @Inject
   ConfigNotesMigration(@GerritServerConfig Config cfg) {
     checkConfig(cfg);
-    writeChanges = cfg.getBoolean(NOTEDB, Table.CHANGES.key(), WRITE, false);
-    readChanges = cfg.getBoolean(NOTEDB, Table.CHANGES.key(), READ, false);
+    writeChanges = cfg.getBoolean(NOTE_DB, CHANGES.key(), WRITE, false);
+    readChanges = cfg.getBoolean(NOTE_DB, CHANGES.key(), READ, false);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index b0cb3e9..2dd9d7e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -24,14 +24,12 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -43,45 +41,22 @@
  * its drafts branch.
  */
 public class DraftCommentNotes extends AbstractChangeNotes<DraftCommentNotes> {
-  @Singleton
-  public static class Factory {
-    private final GitRepositoryManager repoManager;
-    private final NotesMigration migration;
-    private final AllUsersName draftsProject;
-    private final ChangeNoteUtil noteUtil;
-
-    @VisibleForTesting
-    @Inject
-    public Factory(GitRepositoryManager repoManager,
-        NotesMigration migration,
-        AllUsersName allUsers,
-        ChangeNoteUtil noteUtil) {
-      this.repoManager = repoManager;
-      this.migration = migration;
-      this.draftsProject = allUsers;
-      this.noteUtil = noteUtil;
-    }
-
-    public DraftCommentNotes create(Change.Id changeId, Account.Id accountId) {
-      return new DraftCommentNotes(repoManager, migration, draftsProject,
-          noteUtil, changeId, accountId);
-    }
+  public interface Factory {
+    DraftCommentNotes create(Change.Id changeId, Account.Id accountId);
   }
 
-  private final AllUsersName draftsProject;
-  private final ChangeNoteUtil noteUtil;
   private final Account.Id author;
 
   private ImmutableListMultimap<RevId, PatchLineComment> comments;
   private RevisionNoteMap revisionNoteMap;
 
-  DraftCommentNotes(GitRepositoryManager repoManager, NotesMigration migration,
-      AllUsersName draftsProject, ChangeNoteUtil noteUtil, Change.Id changeId,
-      Account.Id author) {
-    super(repoManager, migration, changeId);
-    this.draftsProject = draftsProject;
+  @AssistedInject
+  DraftCommentNotes(
+      Args args,
+      @Assisted Change.Id changeId,
+      @Assisted Account.Id author) {
+    super(args, changeId);
     this.author = author;
-    this.noteUtil = noteUtil;
   }
 
   RevisionNoteMap getRevisionNoteMap() {
@@ -112,33 +87,26 @@
   }
 
   @Override
-  protected void onLoad() throws IOException, ConfigInvalidException {
+  protected void onLoad(RevWalk walk)
+      throws IOException, ConfigInvalidException {
     ObjectId rev = getRevision();
     if (rev == null) {
       loadDefaults();
       return;
     }
 
-    try (RevWalk walk = new RevWalk(reader)) {
-      RevCommit tipCommit = walk.parseCommit(rev);
-      revisionNoteMap = RevisionNoteMap.parse(
-          noteUtil, getChangeId(), reader, NoteMap.read(reader, tipCommit),
-          true);
-      Multimap<RevId, PatchLineComment> cs = ArrayListMultimap.create();
-      for (RevisionNote rn : revisionNoteMap.revisionNotes.values()) {
-        for (PatchLineComment c : rn.comments) {
-          cs.put(c.getRevId(), c);
-        }
+    RevCommit tipCommit = walk.parseCommit(rev);
+    ObjectReader reader = walk.getObjectReader();
+    revisionNoteMap = RevisionNoteMap.parse(
+        args.noteUtil, getChangeId(), reader, NoteMap.read(reader, tipCommit),
+        true);
+    Multimap<RevId, PatchLineComment> cs = ArrayListMultimap.create();
+    for (RevisionNote rn : revisionNoteMap.revisionNotes.values()) {
+      for (PatchLineComment c : rn.comments) {
+        cs.put(c.getRevId(), c);
       }
-      comments = ImmutableListMultimap.copyOf(cs);
     }
-  }
-
-  @Override
-  protected boolean onSave(CommitBuilder commit) throws IOException,
-      ConfigInvalidException {
-    throw new UnsupportedOperationException(
-        getClass().getSimpleName() + " is read-only");
+    comments = ImmutableListMultimap.copyOf(cs);
   }
 
   @Override
@@ -148,7 +116,7 @@
 
   @Override
   public Project.NameKey getProjectName() {
-    return draftsProject;
+    return args.allUsers;
   }
 
   @VisibleForTesting
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbMetrics.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
new file mode 100644
index 0000000..4839891
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbMetrics.java
@@ -0,0 +1,67 @@
+// 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.notedb;
+
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer1;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+class NoteDbMetrics {
+  /** End-to-end latency for writing a collection of updates. */
+  final Timer1<NoteDbTable> updateLatency;
+
+  /**
+   * End-to-end latency for reading changes from NoteDb, including reading
+   * ref(s) and parsing.
+   */
+  final Timer1<NoteDbTable> readLatency;
+
+  /**
+   * The portion of {@link #readLatency} due to parsing commits, but excluding
+   * I/O (to a best effort).
+   */
+  final Timer1<NoteDbTable> parseLatency;
+
+  @Inject
+  NoteDbMetrics(MetricMaker metrics) {
+    Field<NoteDbTable> view = Field.ofEnum(NoteDbTable.class, "table");
+
+    updateLatency = metrics.newTimer(
+        "notedb/update_latency",
+        new Description("NoteDb update latency by table")
+            .setCumulative()
+            .setUnit(Units.MILLISECONDS),
+        view);
+
+    readLatency = metrics.newTimer(
+        "notedb/read_latency",
+        new Description("NoteDb read latency by table")
+            .setCumulative()
+            .setUnit(Units.MILLISECONDS),
+        view);
+
+    parseLatency = metrics.newTimer(
+        "notedb/parse_latency",
+        new Description("NoteDb parse latency by table")
+            .setCumulative()
+            .setUnit(Units.MICROSECONDS),
+        view);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
index 6d84b41..6b8caca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -21,6 +21,7 @@
   public void configure() {
     factory(ChangeUpdate.Factory.class);
     factory(ChangeDraftUpdate.Factory.class);
+    factory(DraftCommentNotes.Factory.class);
     factory(NoteDbUpdateManager.Factory.class);
   }
 }
diff --git a/polygerrit-ui/app/test/fake-app.js b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbTable.java
similarity index 68%
copy from polygerrit-ui/app/test/fake-app.js
copy to gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbTable.java
index 5ee6915..b0c8432 100644
--- a/polygerrit-ui/app/test/fake-app.js
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbTable.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2015 The Android Open Source Project
+// 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.
@@ -12,14 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-'use strict';
+package com.google.gerrit.server.notedb;
 
-/**
- * A stub of the global gr-app element. Use this for testing.
- */
-var app = {
-  accountReady: {
-    then: function(cb) { return cb(); },
-  },
-  loggedIn: false,
-};
+enum NoteDbTable {
+  CHANGES;
+
+  String key() {
+    return name().toLowerCase();
+  }
+
+  @Override
+  public String toString() {
+    return key();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index c797fed..f6f5f2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -18,9 +18,11 @@
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ListMultimap;
+import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.ChainedReceiveCommands;
@@ -38,6 +40,8 @@
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
 
 public class NoteDbUpdateManager {
   public interface Factory {
@@ -73,6 +77,7 @@
   private final GitRepositoryManager repoManager;
   private final NotesMigration migration;
   private final AllUsersName allUsersName;
+  private final NoteDbMetrics metrics;
   private final Project.NameKey projectName;
   private final ListMultimap<String, ChangeUpdate> changeUpdates;
   private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
@@ -84,10 +89,12 @@
   NoteDbUpdateManager(GitRepositoryManager repoManager,
       NotesMigration migration,
       AllUsersName allUsersName,
+      NoteDbMetrics metrics,
       @Assisted Project.NameKey projectName) {
     this.repoManager = repoManager;
     this.migration = migration;
     this.allUsersName = allUsersName;
+    this.metrics = metrics;
     this.projectName = projectName;
     changeUpdates = ArrayListMultimap.create();
     draftUpdates = ArrayListMultimap.create();
@@ -186,7 +193,7 @@
     if (isEmpty()) {
       return;
     }
-    try {
+    try (Timer1.Context timer = metrics.updateLatency.start(CHANGES)) {
       initChangeRepo();
       if (!draftUpdates.isEmpty()) {
         initAllUsersRepo();
@@ -243,15 +250,23 @@
     }
   }
 
-  private static void addUpdates(
-      ListMultimap<String, ? extends AbstractChangeUpdate> updates, OpenRepo or)
+  private static <U extends AbstractChangeUpdate> void addUpdates(
+      ListMultimap<String, U> all, OpenRepo or)
       throws OrmException, IOException {
-    for (String refName : updates.keySet()) {
+    for (Map.Entry<String, Collection<U>> e : all.asMap().entrySet()) {
+      String refName = e.getKey();
+      Collection<U> updates = e.getValue();
       ObjectId old = firstNonNull(
           or.cmds.getObjectId(or.repo, refName), ObjectId.zeroId());
-      ObjectId curr = old;
+      // Only actually write to the ref if one of the updates explicitly allows
+      // us to do so, i.e. it is known to represent a new change. This avoids
+      // writing partial change meta if the change hasn't been backfilled yet.
+      if (!allowWrite(updates, old)) {
+        continue;
+      }
 
-      for (AbstractChangeUpdate u : updates.get(refName)) {
+      ObjectId curr = old;
+      for (U u : updates) {
         ObjectId next = u.apply(or.rw, or.ins, curr);
         if (next == null) {
           continue;
@@ -263,4 +278,12 @@
       }
     }
   }
+
+  private static <U extends AbstractChangeUpdate> boolean allowWrite(
+      Collection<U> updates, ObjectId old) {
+    if (!old.equals(ObjectId.zeroId())) {
+      return true;
+    }
+    return updates.iterator().next().allowWriteToNewRef();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PatchSetState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PatchSetState.java
index 7fa7cff..39cd6cc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PatchSetState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PatchSetState.java
@@ -24,7 +24,7 @@
   /**
    * Deleted patch set.
    * <p>
-   * Used internally as a tombstone; patch sets exposed by public notedb
+   * Used internally as a tombstone; patch sets exposed by public NoteDb
    * interfaces never have this state.
    */
   DELETED;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 3b63f95..4bda245 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -236,6 +236,12 @@
         ) && !isPatchSetLocked(db);
   }
 
+  /** Can this user change the destination branch of this change
+      to the new ref? */
+  public boolean canMoveTo(String ref, ReviewDb db) throws OrmException {
+    return getProjectControl().controlForRef(ref).canUpload() && canAbandon(db);
+  }
+
   /** Can this user publish this draft change or any draft patch set of this change? */
   public boolean canPublish(final ReviewDb db) throws OrmException {
     return (isOwner() || getRefControl().canPublishDrafts())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
index 7da38ed..bda2c71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -19,15 +19,12 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.DeleteBranch.Input;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -52,19 +49,17 @@
 
   private final Provider<IdentifiedUser> identifiedUser;
   private final GitRepositoryManager repoManager;
-  private final Provider<ReviewDb> dbProvider;
   private final Provider<InternalChangeQuery> queryProvider;
   private final GitReferenceUpdated referenceUpdated;
   private final ChangeHooks hooks;
 
   @Inject
   DeleteBranch(Provider<IdentifiedUser> identifiedUser,
-      GitRepositoryManager repoManager, Provider<ReviewDb> dbProvider,
+      GitRepositoryManager repoManager,
       Provider<InternalChangeQuery> queryProvider,
       GitReferenceUpdated referenceUpdated, ChangeHooks hooks) {
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
-    this.dbProvider = dbProvider;
     this.queryProvider = queryProvider;
     this.referenceUpdated = referenceUpdated;
     this.hooks = hooks;
@@ -115,9 +110,6 @@
         case FORCED:
           referenceUpdated.fire(rsrc.getNameKey(), u, ReceiveCommand.Type.DELETE);
           hooks.doRefUpdatedHook(rsrc.getBranchKey(), u, identifiedUser.get().getAccount());
-          ResultSet<SubmoduleSubscription> submoduleSubscriptions =
-            dbProvider.get().submoduleSubscriptions().bySuperProject(rsrc.getBranchKey());
-          dbProvider.get().submoduleSubscriptions().delete(submoduleSubscriptions);
           break;
 
         case REJECTED_CURRENT_BRANCH:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
index 7ec0a6f..b851f9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -22,15 +22,12 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.DeleteBranches.Input;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -69,7 +66,6 @@
 
   private final Provider<IdentifiedUser> identifiedUser;
   private final GitRepositoryManager repoManager;
-  private final Provider<ReviewDb> dbProvider;
   private final Provider<InternalChangeQuery> queryProvider;
   private final GitReferenceUpdated referenceUpdated;
   private final ChangeHooks hooks;
@@ -77,13 +73,11 @@
   @Inject
   DeleteBranches(Provider<IdentifiedUser> identifiedUser,
       GitRepositoryManager repoManager,
-      Provider<ReviewDb> dbProvider,
       Provider<InternalChangeQuery> queryProvider,
       GitReferenceUpdated referenceUpdated,
       ChangeHooks hooks) {
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
-    this.dbProvider = dbProvider;
     this.queryProvider = queryProvider;
     this.referenceUpdated = referenceUpdated;
     this.hooks = hooks;
@@ -167,15 +161,11 @@
     errorMessages.append("\n");
   }
 
-  private void postDeletion(ProjectResource project, ReceiveCommand cmd)
-      throws OrmException {
+  private void postDeletion(ProjectResource project, ReceiveCommand cmd) {
     referenceUpdated.fire(project.getNameKey(), cmd);
     Branch.NameKey branchKey =
         new Branch.NameKey(project.getNameKey(), cmd.getRefName());
     hooks.doRefUpdatedHook(branchKey, cmd.getOldId(), cmd.getNewId(),
         identifiedUser.get().getAccount());
-    ResultSet<SubmoduleSubscription> submoduleSubscriptions =
-        dbProvider.get().submoduleSubscriptions().bySuperProject(branchKey);
-    dbProvider.get().submoduleSubscriptions().delete(submoduleSubscriptions);
   }
 }
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 0db99e6..40fadb4 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
@@ -281,7 +281,7 @@
     ChangeData create(ReviewDb db, ChangeControl c);
 
     // TODO(dborowitz): Remove when deleting index schemas <27.
-    ChangeData createOnlyWhenNotedbDisabled(ReviewDb db, Change.Id id);
+    ChangeData createOnlyWhenNoteDbDisabled(ReviewDb db, Change.Id id);
   }
 
   /**
@@ -506,7 +506,7 @@
       @Assisted ReviewDb db,
       @Assisted Change.Id id) {
     checkState(!notesMigration.readChanges(),
-        "do not call createOnlyWhenNotedbDisabled when notedb is enabled");
+        "do not call createOnlyWhenNoteDbDisabled when NoteDb is enabled");
     this.db = db;
     this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
@@ -629,7 +629,7 @@
   public Project.NameKey project() throws OrmException {
     if (project == null) {
       checkState(!notesMigration.readChanges(), "should not have created "
-          + " ChangeData without a project when notedb is enabled");
+          + " ChangeData without a project when NoteDb is enabled");
       project = change().getProject();
     }
     return project;
@@ -699,7 +699,7 @@
 
   public Change reloadChange() throws OrmException {
     if (project == null) {
-      notes = notesFactory.createFromIdOnlyWhenNotedbDisabled(db, legacyId);
+      notes = notesFactory.createFromIdOnlyWhenNoteDbDisabled(db, legacyId);
     } else {
       notes = notesFactory.create(db, project, legacyId);
     }
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 3501374..658f3bb 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
@@ -32,7 +32,7 @@
 import com.google.gwtorm.server.ResultSet;
 
 public class DisabledChangesReviewDbWrapper extends ReviewDbWrapper {
-  private static final String MSG = "This table has been migrated to notedb";
+  private static final String MSG = "This table has been migrated to NoteDb";
 
   private final DisabledChangeAccess changes;
   private final DisabledPatchSetApprovalAccess patchSetApprovals;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbFactory.java
index 272ab72..3a63360 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbFactory.java
@@ -25,7 +25,7 @@
  * that talks to the underlying traditional {@link
  * com.google.gerrit.reviewdb.server.ReviewDb} database.
  * <p>
- * During the migration to notedb, the actual {@code ReviewDb} will be a wrapper
+ * During the migration to NoteDb, the actual {@code ReviewDb} will be a wrapper
  * with certain tables enabled/disabled; this marker goes on the low-level
  * implementation that has all tables.
  */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 310cae8..edeb583 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_119> C = Schema_119.class;
+  public static final Class<Schema_120> C = Schema_120.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_116.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_116.java
index c7c2a59..5b018a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_116.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_116.java
@@ -14,30 +14,12 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.sql.SQLException;
-import java.sql.Statement;
-
 public class Schema_116 extends SchemaVersion {
   @Inject
   Schema_116(Provider<Schema_115> prior) {
     super(prior);
   }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    ui.message("Migrate user preference copySelfOnEmail to emailStrategy");
-    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement()) {
-      stmt.executeUpdate("UPDATE accounts SET "
-          + "EMAIL_STRATEGY='ENABLED' "
-          + "WHERE (COPY_SELF_ON_EMAIL='N')");
-      stmt.executeUpdate("UPDATE accounts SET "
-          + "EMAIL_STRATEGY='CC_ON_OWN_COMMENTS' "
-          + "WHERE (COPY_SELF_ON_EMAIL='Y')");
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java
index 6fc7c83..4f6620e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java
@@ -53,11 +53,13 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
+import java.sql.Connection;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
 
 public class Schema_119 extends SchemaVersion {
   private static final Map<String, String> LEGACY_DISPLAYNAME_MAP =
@@ -86,6 +88,12 @@
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui)
       throws OrmException, SQLException {
+    JdbcSchema schema = (JdbcSchema) db;
+    Connection connection = schema.getConnection();
+    String tableName = "accounts";
+    String emailStrategy = "email_strategy";
+    Set<String> columns =
+        schema.getDialect().listColumns(connection, tableName);
     Map<Account.Id, GeneralPreferencesInfo> imports = new HashMap<>();
     try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
         ResultSet rs = stmt.executeQuery(
@@ -96,7 +104,9 @@
           + "use_flash_clipboard, "
           + "download_url, "
           + "download_command, "
-          + "email_strategy, "
+          + (columns.contains(emailStrategy)
+              ? emailStrategy + ", "
+              : "copy_self_on_email, ")
           + "date_format, "
           + "time_format, "
           + "relative_date_in_change_table, "
@@ -105,7 +115,7 @@
           + "legacycid_in_change_table, "
           + "review_category_strategy, "
           + "mute_common_path_prefixes "
-          + "from accounts")) {
+          + "from " + tableName)) {
         while (rs.next()) {
           GeneralPreferencesInfo p =
               new GeneralPreferencesInfo();
@@ -115,7 +125,8 @@
           p.useFlashClipboard = toBoolean(rs.getString(4));
           p.downloadScheme = convertToModernNames(rs.getString(5));
           p.downloadCommand = toDownloadCommand(rs.getString(6));
-          p.emailStrategy = toEmailStrategy(rs.getString(7));
+          p.emailStrategy = toEmailStrategy(rs.getString(7),
+              columns.contains(emailStrategy));
           p.dateFormat = toDateFormat(rs.getString(8));
           p.timeFormat = toTimeFormat(rs.getString(9));
           p.relativeDateInChangeTable = toBoolean(rs.getString(10));
@@ -191,11 +202,25 @@
     return DiffView.valueOf(v);
   }
 
-  private static EmailStrategy toEmailStrategy(String v) {
+  private static EmailStrategy toEmailStrategy(String v,
+      boolean emailStrategyColumnExists) throws OrmException {
     if (v == null) {
       return EmailStrategy.ENABLED;
     }
-    return EmailStrategy.valueOf(v);
+    if (emailStrategyColumnExists) {
+      return EmailStrategy.valueOf(v);
+    } else {
+      if (v.equals("N")) {
+        // EMAIL_STRATEGY='ENABLED' WHERE (COPY_SELF_ON_EMAIL='N')
+        return EmailStrategy.ENABLED;
+      } else if (v.equals("Y")) {
+        // EMAIL_STRATEGY='CC_ON_OWN_COMMENTS' WHERE (COPY_SELF_ON_EMAIL='Y')
+        return EmailStrategy.CC_ON_OWN_COMMENTS;
+      } else {
+        throw new OrmException(
+            "invalid value in accounts.copy_self_on_email: " + v);
+      }
+    }
   }
 
   private static ReviewCategoryStrategy toReviewCategoryStrategy(String v) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
new file mode 100644
index 0000000..df6e8fb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_120.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.common.data.SubscribeSection;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.RefSpec;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_120 extends SchemaVersion {
+
+  private final GitRepositoryManager mgr;
+
+  @Inject
+  Schema_120(Provider<Schema_119> prior,
+      GitRepositoryManager mgr) {
+    super(prior);
+    this.mgr = mgr;
+  }
+
+  private void allowSubmoduleSubscription(Branch.NameKey subbranch,
+      Branch.NameKey superBranch) throws OrmException {
+    try (Repository git = mgr.openRepository(subbranch.getParentKey());
+        RevWalk rw = new RevWalk(git)) {
+      BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
+      try(MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+          subbranch.getParentKey(), git, bru)) {
+        md.setMessage("Added superproject subscription during upgrade");
+        ProjectConfig pc = ProjectConfig.read(md);
+
+        SubscribeSection s = null;
+        for (SubscribeSection s1 : pc.getSubscribeSections(subbranch)) {
+          if (s.getProject() == superBranch.getParentKey()) {
+            s = s1;
+          }
+        }
+        if (s == null) {
+          s = new SubscribeSection(superBranch.getParentKey());
+          pc.addSubscribeSection(s);
+        }
+        RefSpec newRefSpec = new RefSpec(subbranch.get() + ":" + superBranch.get());
+
+        if (!s.getRefSpecs().contains(newRefSpec)) {
+          // For the migration we use only exact RefSpecs, we're not trying to
+          // generalize it.
+          s.addRefSpec(newRefSpec);
+        }
+
+        pc.commit(md);
+      }
+      bru.execute(rw, NullProgressMonitor.INSTANCE);
+    } catch (ConfigInvalidException | IOException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    ui.message("Generating Superproject subscriptions table to submodule ACLs");
+
+    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+        ResultSet rs = stmt.executeQuery("SELECT "
+            + "super_project_project_name, "
+            + "super_project_branch_name, "
+            + "submodule_project_name, "
+            + "submodule_branch_name "
+            + "FROM submodule_subscriptions");) {
+      while (rs.next()) {
+        Project.NameKey superproject = new Project.NameKey(rs.getString(1));
+        Branch.NameKey superbranch = new Branch.NameKey(superproject,
+            rs.getString(2));
+
+        Project.NameKey submodule = new Project.NameKey(rs.getString(4));
+        Branch.NameKey subbranch = new Branch.NameKey(submodule,
+            rs.getString(5));
+
+        allowSubmoduleSubscription(subbranch, superbranch);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.vm
new file mode 100644
index 0000000..294063e
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.vm
@@ -0,0 +1,44 @@
+## 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.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## Gerrit will use templates ending in ".vm" but will ignore templates ending
+## in ".vm.example".  If a .vm template does not exist, the default internal
+## gerrit template which is the same as the .vm.example will be used.  If you
+## want to override the default template, copy the .vm.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The DeleteVote.vm template will determine the contents of the email related
+## to removing votes on changes.  It is a ChangeEmail: see ChangeSubject.vm
+## and ChangeFooter.vm.
+##
+$fromName has removed a vote on this change.
+
+Change subject: $change.subject
+......................................................................
+
+
+#if ($coverLetter)
+$coverLetter
+
+#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
index 823455d..02978da 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -1,57 +1,247 @@
+apl = text/apl
 as = text/x-gas
+asn = text/x-ttcn-asn
+asn1 = text/x-ttcn-asn
+asp = application/x-aspx
+aspx = application/x-aspx
+asterisk = text/x-asterisk
+b = text/x-brainfuck
+bash = text/x-sh
+bf = text/x-brainfuck
+bnf = text/x-ebnf
 bucklet = text/x-python
+bzl = text/x-python
 BUCK = text/x-python
-clj = text/x-clojure
+BUILD = text/x-python
+c = text/x-csrc
+cfg = text/x-ttcn-cfg
 cl = text/x-common-lisp
+clj = text/x-clojure
+cljs = text/x-clojurescript
+cmake = text/x-cmake
+cmake.in = text/x-cmake
+contributing.md = text/x-gfm
+CMakeLists.txt = text/x-cmake
+CONTRIBUTING.md = text/x-gfm
+cob = text/x-cobol
 coffee = text/x-coffeescript
+conf = text/plain
+cpy = text/x-cobol
+cr = text/x-crystal
 cs = text/x-csharp
+csharp = text/x-csharp
+css = text/css
 cpp = text/x-c++src
+cql = text/x-cassandra
 cxx = text/x-c++src
+cyp = application/x-cypher-query
+cypher = application/x-cypher-query
+c++ = text/x-c++src
 d = text/x-d
 dart = application/dart
+def = text/plain
 defs = text/x-python
 diff = text/x-diff
+django = text/x-django
+dtd = application/xml-dtd
+dyalog = text/apl
+dyl = text/x-dylan
+dylan = text/x-dylan
 Dockerfile = text/x-dockerfile
 dtd = application/xml-dtd
+e = text/x-eiffel
+ebnf = text/x-ebnf
+ecl = text/x-ecl
 el = text/x-common-lisp
+elm = text/x-elm
+ejs = application/x-ejs
+erb = application/x-erb
 erl = text/x-erlang
+es6 = text/jsx
+excel = text/x-spreadsheet
+extensions.conf = text/x-asterisk
+f = text/x-fortran
+factor = text/x-factor
+feathre = text/x-feature
+fcl = text/x-fcl
+for = text/x-fortran
+formula = text/x-spreadsheet
+forth = text/x-forth
+fth = text/x-forth
 frag = x-shader/x-fragment
+fs = text/x-fsharp
+fsharp = text/x-fsharp
+f77 = text/x-fortran
+f90 = text/x-fortran
 gitmodules = text/x-ini
 glsl = x-shader/x-vertex
 go = text/x-go
 gradle = text/x-groovy
+gradlew = text/x-sh
 groovy = text/x-groovy
-hs = text/x-haskell
+gss = text/x-gss
+h = text/x-csrc
+haml = text/x-haml
+hh = text/x-c++src
+history.md = text/x-gfm
 hpp = text/x-c++src
+hs = text/x-haskell
+htm = text/html
+html = text/html
+http = message/http
+hx = text/x-haxe
+hxml = text/x-hxml
 hxx = text/x-c++src
+h++ = text/x-c++src
+HISTORY.md = text/x-gfm
+in = text/x-properties
+ini = text/x-properties
+intr = text/x-dylan
+jade = text/x-jade
+java = text/x-java
+jl = text/x-julia
+jruby = text/x-ruby
+js = text/javascript
+json = application/json
+jsonld = application/ld+json
+jsx = text/jsx
+jsp = application/x-jsp
+kt = text/x-kotlin
+less = text/x-less
+lhs = text/x-literate-haskell
 lisp = text/x-common-lisp
+list = text/plain
+log = text/plain
+ls = text/x-livescript
 lsp = text/x-common-lisp
 lua = text/x-lua
 m = text/x-objectivec
+macruby = text/x-ruby
+map = application/json
+markdown = text/x-markdown
 md = text/x-markdown
+mirc = text/mirc
+mkd = text/x-markdown
+ml = text/x-ocaml
+mli = text/x-ocaml
+mll = text/x-ocaml
+mly = text/x-ocaml
+mm = text/x-objectivec
+mo = text/x-modelica
+mps = text/x-mumps
+msc = text/x-mscgen
+mscgen = text/x-mscgen
+mscin = text/x-mscgen
+msgenny = text/x-msgenny
+nb = text/x-mathematica
+nginx.conf = text/x-nginx-conf
+nsh = text/x-nsis
+nsi = text/x-nsis
+nt = text/n-triples
+nut = text/x-squirrel
+oz = text/x-oz
+p = text/x-pascal
+pas = text/x-pascal
 patch = text/x-diff
+pgp = application/pgp
 php = text/x-php
+php3 = text/x-php
+php4 = text/x-php
+php5 = text/x-php
+phtml = text/x-php
 pig = text/x-pig
 pl = text/x-perl
+pls = text/x-plsql
 pm = text/x-perl
 pp = text/x-puppet
+pro = text/x-idl
 project.config = text/x-ini
 properties = text/x-ini
+proto = text/x-protobuf
+protobuf = text/x-protobuf
 py = text/x-python
+pyw = text/x-python
+pyx = text/x-cython
+pxd = text/x-cython
+pxi = text/x-cython
+PKGBUILD = text/x-sh
+q = text/x-q
 r = text/r-src
+rake = text/x-ruby
 rb = text/x-ruby
+rbx = text/x-ruby
+readme.md = text/x-gfm
 rng = application/xml
+rpm = text/x-rpm-changes
+rq = application/sparql-query
+rs = text/x-rustsrc
+rss = application/xml
 rst = text/x-rst
+README.md = text/x-gfm
+s = text/x-gas
+sass = text/x-sass
 scala = text/x-scala
+scm = text/x-scheme
+scss = text/x-scss
+sh = text/x-sh
+sieve = application/sieve
+siv = application/sieve
+slim = text/x-slim
+solr = text/x-solr
 soy = text/x-soy
+sparql = application/sparql-query
+sparul = applicatoin/sparql-query
+spec = text/x-rpm-spec
+spreadsheet = text/x-spreadsheet
+sql = text/x-sql
+ss = text/x-scheme
 st = text/x-stsrc
 stex = text/x-stex
 swift = text/x-swift
 tcl = text/x-tcl
+tex = text/x-latex
+text = text/plain
+textile = text/x-textile
+tiddly = text/x-tiddlywiki
+tiddlywiki = text/x-tiddlywiki
+tiki = text/tiki
+toml = text/x-toml
+tpl = text/x-smarty
+ts = application/typescript
+ttcn = text/x-ttcn
+ttcnpp = text/x-ttcn
+ttcn3 = text/x-ttcn
+ttl = text/turtle
+txt = text/plain
+twig = text/x-twig
 v = text/x-verilog
+vb = text/x-vb
+vbs = text/vbscript
 vert = x-shader/x-vertex
 vh = text/x-verilog
+vhd = text/x-vhdl
 vhdl = text/x-vhdl
 vm = text/velocity
+vtl = text/velocity
+wsdl = application/xml
+xhtml = text/html
+xml = application/xml
+xsd = application/xml
+xsl = application/xml
+xquery = application/xquery
+xu = text/x-xu
+xy = application/xquery
 yaml = text/x-yaml
 yml = text/x-yaml
+zsh = text/x-sh
+z80 = text/x-z80
+1 = text/troff
+2 = text/troff
+3 = text/troff
+4 = text/troff
+4th = text/x-forth
+5 = text/troff
+6 = text/troff
+7 = text/troff
+8 = text/troff
+9 = text/troff
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index b387405..2a40b57 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -21,6 +21,8 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.metrics.DisabledMetricMaker;
+import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.CommentRange;
@@ -110,6 +112,9 @@
   @Inject
   protected ChangeNoteUtil noteUtil;
 
+  @Inject
+  protected AbstractChangeNotes.Args args;
+
   private Injector injector;
   private String systemTimeZone;
 
@@ -139,7 +144,7 @@
       @Override
       public void configure() {
         install(new GitModule());
-        factory(NoteDbUpdateManager.Factory.class);
+        install(new NoteDbModule());
         bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
         bind(String.class).annotatedWith(GerritServerId.class)
             .toInstance("gerrit");
@@ -165,6 +170,7 @@
             .toInstance(GitReferenceUpdated.DISABLED);
         bind(StarredChangesUtil.class)
             .toProvider(Providers.<StarredChangesUtil> of(null));
+        bind(MetricMaker.class).to(DisabledMetricMaker.class);
       }
     });
 
@@ -198,14 +204,13 @@
 
   protected ChangeUpdate newUpdate(Change c, CurrentUser user)
       throws Exception {
-    ChangeUpdate update = TestChanges.newUpdate(
-        injector, repoManager, MIGRATION, c, allUsers, user);
+    ChangeUpdate update = TestChanges.newUpdate(injector, c, user);
+    update.setAllowWriteToNewRef(true);
     return update;
   }
 
   protected ChangeNotes newNotes(Change c) throws OrmException {
-    return new ChangeNotes(repoManager, MIGRATION, allUsers, noteUtil,
-        c.getProject(), c).load();
+    return new ChangeNotes(args, c.getProject(), c).load();
   }
 
   protected static SubmitRecord submitRecord(String status,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 729ea7d..2968a62 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -462,6 +462,6 @@
   private ChangeNotesParser newParser(ObjectId tip) throws Exception {
     Change c = newChange();
     return new ChangeNotesParser(c.getProject(), c.getId(), tip, walk,
-        repoManager, noteUtil);
+        repoManager, noteUtil, args.metrics);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 9a14c19..7738baa 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -909,8 +909,9 @@
     assertThat(commitWithComments).isNotNull();
 
     try (RevWalk rw = new RevWalk(repo)) {
-      try (ChangeNotesParser notesWithComments = new ChangeNotesParser(project,
-          c.getId(), commitWithComments.copy(), rw, repoManager, noteUtil)) {
+      try (ChangeNotesParser notesWithComments = new ChangeNotesParser(
+          project, c.getId(), commitWithComments.copy(), rw, repoManager,
+          noteUtil, args.metrics)) {
         notesWithComments.parseAll();
         ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals1 =
             notesWithComments.buildApprovals();
@@ -922,7 +923,7 @@
     try (RevWalk rw = new RevWalk(repo)) {
       try (ChangeNotesParser notesWithApprovals = new ChangeNotesParser(project,
           c.getId(), commitWithApprovals.copy(), rw, repoManager,
-          noteUtil)) {
+          noteUtil, args.metrics)) {
         notesWithApprovals.parseAll();
         ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals2 =
             notesWithApprovals.buildApprovals();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java
index 7bddade..eda2b82 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java
@@ -17,14 +17,13 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import java.util.List;
-
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.EditList;
 import org.eclipse.jgit.diff.ReplaceEdit;
-
 import org.junit.Test;
 
+import java.util.List;
+
 public class IntraLineLoaderTest {
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index e8d95df..129c155 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1019,7 +1019,7 @@
   }
 
   @Test
-  public void byHashtagWithNotedb() throws Exception {
+  public void byHashtagWithNoteDb() throws Exception {
     assume().that(notesMigration.enabled()).isTrue();
     List<Change> changes = setUpHashtagChanges();
     assertQuery("hashtag:foo", changes.get(1), changes.get(0));
@@ -1032,7 +1032,7 @@
   }
 
   @Test
-  public void byHashtagWithoutNotedb() throws Exception {
+  public void byHashtagWithoutNoteDb() throws Exception {
     assume().that(notesMigration.enabled()).isFalse();
     setUpHashtagChanges();
     assertQuery("hashtag:foo");
@@ -1335,7 +1335,7 @@
     cd.reviewedBy();
 
     // TODO(dborowitz): Swap out GitRepositoryManager somehow? Will probably be
-    // necessary for notedb anyway.
+    // necessary for NoteDb anyway.
     cd.isMergeable();
 
     exception.expect(DisabledReviewDb.Disabled.class);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
index c526cea..fa7a5c4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
@@ -33,7 +33,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
 import com.google.gerrit.reviewdb.server.StarredChangeAccess;
-import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
 import com.google.gerrit.reviewdb.server.SystemConfigAccess;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.StatementExecutor;
@@ -164,11 +163,6 @@
   }
 
   @Override
-  public SubmoduleSubscriptionAccess submoduleSubscriptions() {
-    throw new Disabled();
-  }
-
-  @Override
   public AccountGroupByIdAccess accountGroupById() {
     throw new Disabled();
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java
index f3578e9..797f1cb 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.testutil;
 
-import com.google.common.collect.ImmutableList;
-
 import org.eclipse.jgit.lib.Config;
 import org.junit.Rule;
 import org.junit.rules.TestRule;
@@ -23,13 +21,8 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.model.Statement;
 
-import java.util.Collection;
-
 @RunWith(ConfigSuite.class)
 public class GerritServerTests extends GerritBaseTests {
-  private static final Collection<String> ENV_VAR_TRUE_VALUES =
-      ImmutableList.of("yes", "y", "true", "1");
-
   @ConfigSuite.Parameter
   public Config config;
 
@@ -38,15 +31,6 @@
 
   protected TestNotesMigration notesMigration;
 
-  public static boolean isNoteDbTestEnabled() {
-    return isEnvVarTrue("GERRIT_ENABLE_NOTEDB");
-  }
-
-  public static boolean isEnvVarTrue(String name) {
-    String value = System.getenv(name);
-    return value != null && ENV_VAR_TRUE_VALUES.contains(value.toLowerCase());
-  }
-
   @Rule
   public TestRule testRunner = new TestRule() {
     @Override
@@ -66,8 +50,7 @@
   };
 
   public void beforeTest() throws Exception {
-    notesMigration = new TestNotesMigration()
-        .setAllEnabled(isNoteDbTestEnabled());
+    notesMigration = new TestNotesMigration().setFromEnv();
   }
 
   public void afterTest() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
index d86d265..95101ab 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
@@ -14,13 +14,18 @@
 
 package com.google.gerrit.testutil;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import com.google.common.base.Joiner;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeBundle;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeRebuilder;
 import com.google.gerrit.server.schema.DisabledChangesReviewDbWrapper;
@@ -29,6 +34,7 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,6 +47,7 @@
   static final Logger log = LoggerFactory.getLogger(NoteDbChecker.class);
 
   private final Provider<ReviewDb> dbProvider;
+  private final GitRepositoryManager repoManager;
   private final TestNotesMigration notesMigration;
   private final ChangeNotes.Factory notesFactory;
   private final ChangeRebuilder changeRebuilder;
@@ -48,74 +55,128 @@
 
   @Inject
   NoteDbChecker(Provider<ReviewDb> dbProvider,
+      GitRepositoryManager repoManager,
       TestNotesMigration notesMigration,
       ChangeNotes.Factory notesFactory,
       ChangeRebuilder changeRebuilder,
       PatchLineCommentsUtil plcUtil) {
     this.dbProvider = dbProvider;
+    this.repoManager = repoManager;
     this.notesMigration = notesMigration;
     this.notesFactory = notesFactory;
     this.changeRebuilder = changeRebuilder;
     this.plcUtil = plcUtil;
   }
 
-  public void checkAllChanges() throws Exception {
-    checkChanges(
+  public void rebuildAndCheckAllChanges() throws Exception {
+    rebuildAndCheckChanges(
         Iterables.transform(
             unwrapDb().changes().all(),
             ReviewDbUtil.changeIdFunction()));
   }
 
+  public void rebuildAndCheckChanges(Change.Id... changeIds) throws Exception {
+    rebuildAndCheckChanges(Arrays.asList(changeIds));
+  }
+
+  public void rebuildAndCheckChanges(Iterable<Change.Id> changeIds)
+      throws Exception {
+    ReviewDb db = unwrapDb();
+
+    List<ChangeBundle> allExpected = readExpected(changeIds);
+
+    boolean oldWrite = notesMigration.writeChanges();
+    boolean oldRead = notesMigration.readChanges();
+    try {
+      notesMigration.setWriteChanges(true);
+      notesMigration.setReadChanges(true);
+      List<String> msgs = new ArrayList<>();
+      for (ChangeBundle expected : allExpected) {
+        Change c = expected.getChange();
+        try {
+          changeRebuilder.rebuild(db, c.getId());
+        } catch (RepositoryNotFoundException e) {
+          msgs.add("Repository not found for change, cannot convert: " + c);
+        }
+      }
+
+      checkActual(allExpected, msgs);
+    } finally {
+      notesMigration.setReadChanges(oldRead);
+      notesMigration.setWriteChanges(oldWrite);
+    }
+  }
+
   public void checkChanges(Change.Id... changeIds) throws Exception {
     checkChanges(Arrays.asList(changeIds));
   }
 
   public void checkChanges(Iterable<Change.Id> changeIds) throws Exception {
+    checkActual(readExpected(changeIds), new ArrayList<String>());
+  }
+
+  public void assertNoChangeRef(Project.NameKey project, Change.Id changeId)
+      throws Exception {
+    try (Repository repo = repoManager.openMetadataRepository(project)) {
+      assertThat(repo.exactRef(ChangeNoteUtil.changeRefName(changeId)))
+          .isNull();
+    }
+  }
+
+  private List<ChangeBundle> readExpected(Iterable<Change.Id> changeIds)
+      throws Exception {
     ReviewDb db = unwrapDb();
+    boolean old = notesMigration.readChanges();
+    try {
+      notesMigration.setReadChanges(false);
+      List<Change.Id> sortedIds =
+          ReviewDbUtil.intKeyOrdering().sortedCopy(changeIds);
+      List<ChangeBundle> expected = new ArrayList<>(sortedIds.size());
+      for (Change.Id id : sortedIds) {
+        expected.add(ChangeBundle.fromReviewDb(db, id));
+      }
+      return expected;
+    } finally {
+      notesMigration.setReadChanges(old);
+    }
+  }
 
-    notesMigration.setReadChanges(false);
-    List<Change.Id> sortedIds =
-        ReviewDbUtil.intKeyOrdering().sortedCopy(changeIds);
-    List<ChangeBundle> allExpected = new ArrayList<>(sortedIds.size());
-    for (Change.Id id : sortedIds) {
-      allExpected.add(ChangeBundle.fromReviewDb(db, id));
-    }
-
-    notesMigration.setWriteChanges(true);
-    notesMigration.setReadChanges(true);
-    List<String> all = new ArrayList<>();
-    for (ChangeBundle expected : allExpected) {
-      Change c = expected.getChange();
-      try {
-        changeRebuilder.rebuild(db, c.getId());
-      } catch (RepositoryNotFoundException e) {
-        all.add("Repository not found for change, cannot convert: " + c);
+  private void checkActual(List<ChangeBundle> allExpected, List<String> msgs)
+      throws Exception {
+    ReviewDb db = unwrapDb();
+    boolean oldRead = notesMigration.readChanges();
+    boolean oldWrite = notesMigration.writeChanges();
+    try {
+      notesMigration.setWriteChanges(true);
+      notesMigration.setReadChanges(true);
+      for (ChangeBundle expected : allExpected) {
+        Change c = expected.getChange();
+        ChangeBundle actual;
+        try {
+          actual = ChangeBundle.fromNotes(
+              plcUtil, notesFactory.create(db, c.getProject(), c.getId()));
+        } catch (Throwable t) {
+          String msg = "Error converting change: " + c;
+          msgs.add(msg);
+          log.error(msg, t);
+          continue;
+        }
+        List<String> diff = expected.differencesFrom(actual);
+        if (!diff.isEmpty()) {
+          msgs.add("Differences between ReviewDb and NoteDb for " + c + ":");
+          msgs.addAll(diff);
+          msgs.add("");
+        } else {
+          System.err.println(
+              "NoteDb conversion of change " + c.getId() + " successful");
+        }
       }
+    } finally {
+      notesMigration.setReadChanges(oldRead);
+      notesMigration.setWriteChanges(oldWrite);
     }
-    for (ChangeBundle expected : allExpected) {
-      Change c = expected.getChange();
-      ChangeBundle actual;
-      try {
-        actual = ChangeBundle.fromNotes(
-            plcUtil, notesFactory.create(db, c.getProject(), c.getId()));
-      } catch (Throwable t) {
-        String msg = "Error converting change: " + c;
-        all.add(msg);
-        log.error(msg, t);
-        continue;
-      }
-      List<String> diff = expected.differencesFrom(actual);
-      if (!diff.isEmpty()) {
-        all.add("Differences between ReviewDb and NoteDb for " + c + ":");
-        all.addAll(diff);
-        all.add("");
-      } else {
-        System.err.println(
-            "NoteDb conversion of change " + c.getId() + " successful");
-      }
-    }
-    if (!all.isEmpty()) {
-      throw new AssertionError(Joiner.on('\n').join(all));
+    if (!msgs.isEmpty()) {
+      throw new AssertionError(Joiner.on('\n').join(msgs));
     }
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
new file mode 100644
index 0000000..103fee3
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
@@ -0,0 +1,66 @@
+// 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.testutil;
+
+import com.google.common.base.Enums;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+public enum NoteDbMode {
+  /** NoteDb is disabled. */
+  OFF,
+
+  /** Writing data to NoteDb is enabled. */
+  WRITE,
+
+  /** Reading and writing all data to NoteDb is enabled. */
+  READ_WRITE,
+
+  /**
+   * Run tests with NoteDb disabled, then convert ReviewDb to NoteDb and check
+   * that the results match.
+   */
+  CHECK;
+
+  private static final String VAR = "GERRIT_NOTEDB";
+
+  public static NoteDbMode get() {
+    if (isEnvVarTrue("GERRIT_ENABLE_NOTEDB")) {
+      // TODO(dborowitz): Remove once GerritForge CI is migrated.
+      return READ_WRITE;
+    }
+    String value = System.getenv(VAR);
+    if (Strings.isNullOrEmpty(value)) {
+      return OFF;
+    }
+    value = value.toUpperCase().replace("-", "_");
+    Optional<NoteDbMode> mode = Enums.getIfPresent(NoteDbMode.class, value);
+    if (!mode.isPresent()) {
+      throw new IllegalArgumentException(
+          "Invalid value for " + VAR + ": " + System.getenv(VAR));
+    }
+    return mode.get();
+  }
+
+  public static boolean readWrite() {
+    return get() == READ_WRITE;
+  }
+
+  private static boolean isEnvVarTrue(String name) {
+    String value = Strings.nullToEmpty(System.getenv(name)).toLowerCase();
+    return ImmutableList.of("yes", "y", "true", "1").contains(value);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
index 386b724..7ac6cbe 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -28,10 +28,8 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.ChangeDraftUpdate;
-import com.google.gerrit.server.notedb.ChangeNoteUtil;
+import com.google.gerrit.server.notedb.AbstractChangeNotes;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -88,34 +86,33 @@
   }
 
   public static ChangeUpdate newUpdate(Injector injector,
-      GitRepositoryManager repoManager, NotesMigration migration, Change c,
-      final AllUsersName allUsers, final CurrentUser user)
-      throws Exception  {
+      Change c, final CurrentUser user) throws Exception  {
     injector = injector.createChildInjector(new FactoryModule() {
       @Override
       public void configure() {
-        factory(ChangeUpdate.Factory.class);
-        factory(ChangeDraftUpdate.Factory.class);
         bind(CurrentUser.class).toInstance(user);
       }
     });
     ChangeUpdate update = injector.getInstance(ChangeUpdate.Factory.class)
         .create(
             stubChangeControl(
-                repoManager, migration, c, allUsers,
-                injector.getInstance(ChangeNoteUtil.class),
+                injector.getInstance(AbstractChangeNotes.Args.class),
+                c,
                 user),
             TimeUtil.nowTs(), Ordering.<String> natural());
 
     ChangeNotes notes = update.getChangeNotes();
     boolean hasPatchSets = notes.getPatchSets() != null
         && !notes.getPatchSets().isEmpty();
+    NotesMigration migration = injector.getInstance(NotesMigration.class);
     if (hasPatchSets || !migration.readChanges()) {
       return update;
     }
 
-    // Change doesn't exist yet. Notedb requires that there be a commit for the
+    // Change doesn't exist yet. NoteDb requires that there be a commit for the
     // first patch set, so create one.
+    GitRepositoryManager repoManager =
+        injector.getInstance(GitRepositoryManager.class);
     try (Repository repo = repoManager.openRepository(c.getProject())) {
       TestRepository<Repository> tr = new TestRepository<>(repo);
       PersonIdent ident = user.asIdentifiedUser()
@@ -136,16 +133,13 @@
   }
 
   private static ChangeControl stubChangeControl(
-      GitRepositoryManager repoManager, NotesMigration migration,
-      Change c, AllUsersName allUsers, ChangeNoteUtil noteUtil,
-      CurrentUser user) throws OrmException {
+      AbstractChangeNotes.Args args,
+      Change c, CurrentUser user) throws OrmException {
     ChangeControl ctl = EasyMock.createMock(ChangeControl.class);
     expect(ctl.getChange()).andStubReturn(c);
     expect(ctl.getProject()).andStubReturn(new Project(c.getProject()));
     expect(ctl.getUser()).andStubReturn(user);
-    ChangeNotes notes =
-        new ChangeNotes(repoManager, migration, allUsers, noteUtil,
-            c.getProject(), c).load();
+    ChangeNotes notes = new ChangeNotes(args, c.getProject(), c).load();
     expect(ctl.getNotes()).andStubReturn(notes);
     expect(ctl.getId()).andStubReturn(c.getId());
     EasyMock.replay(ctl);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
index 032c3e5..34ad8aa 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
@@ -46,4 +46,22 @@
   public TestNotesMigration setAllEnabled(boolean enabled) {
     return setReadChanges(enabled).setWriteChanges(enabled);
   }
+
+  public TestNotesMigration setFromEnv() {
+    switch (NoteDbMode.get()) {
+      case READ_WRITE:
+        setWriteChanges(true);
+        setReadChanges(true);
+        break;
+      case WRITE:
+        setWriteChanges(true);
+        setReadChanges(false);
+        break;
+      case CHECK:
+      case OFF:
+      default:
+        break;
+    }
+    return this;
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
index c508b1d..dc67ac3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
@@ -24,25 +24,19 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.kohsuke.args4j.Argument;
-
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(name = "activate",
   description = "Activate the latest index version available",
   runsAt = MASTER)
 public class IndexActivateCommand extends SshCommand {
 
-  @Argument(index = 0, required = true, metaVar = "INDEX",
-      usage = "index name to activate")
-  private String name;
-
   @Inject
   private LuceneVersionManager luceneVersionManager;
 
   @Override
   protected void run() throws UnloggedFailure {
     try {
-      if (luceneVersionManager.activateLatestIndex(name)) {
+      if (luceneVersionManager.activateLatestIndex()) {
         stdout.println("Activated latest index version");
       } else {
         stdout.println("Not activating index, already using latest version");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
index 1575ed9..1b3b819 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
@@ -24,24 +24,18 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.kohsuke.args4j.Argument;
-
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
 @CommandMetaData(name = "start", description = "Start the online reindexer",
   runsAt = MASTER)
 public class IndexStartCommand extends SshCommand {
 
-  @Argument(index = 0, required = true, metaVar = "INDEX",
-      usage = "index name to activate")
-  private String name;
-
   @Inject
   private LuceneVersionManager luceneVersionManager;
 
   @Override
   protected void run() throws UnloggedFailure {
     try {
-      if (luceneVersionManager.startReindexer(name)) {
+      if (luceneVersionManager.startReindexer()) {
         stdout.println("Reindexer started");
       } else {
         stdout.println("Nothing to reindex, index is already the latest version");
diff --git a/lib/BUCK b/lib/BUCK
index 1458c63..5d6f654 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -16,6 +16,7 @@
 define_license(name = 'jsch')
 define_license(name = 'MPL1.1')
 define_license(name = 'moment')
+define_license(name = 'OFL1.1')
 define_license(name = 'ow2')
 define_license(name = 'page.js')
 define_license(name = 'polymer')
diff --git a/lib/LICENSE-OFL1.1 b/lib/LICENSE-OFL1.1
new file mode 100644
index 0000000..0754257
--- /dev/null
+++ b/lib/LICENSE-OFL1.1
@@ -0,0 +1,93 @@
+Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+
+This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index 9fa905a..6c82406 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -3,8 +3,8 @@
 include_defs('//lib/codemirror/closure.defs')
 
 REPO = MAVEN_CENTRAL
-VERSION = '5.8'
-SHA1 = '1cbe267adf1da9659dae49253305649dae2391e9'
+VERSION = '5.13.2'
+SHA1 = '4a26f060aeca679fdf751d2b480499c8a5f71e47'
 
 if REPO == MAVEN_CENTRAL:
   URL = REPO + 'org/webjars/codemirror/%s/codemirror-%s.jar' % (VERSION, VERSION)
@@ -16,10 +16,11 @@
   ZIP = 'codemirror-%s.zip' % VERSION
 
 
-CLOSURE_VERSION = 'v20160208'
+CLOSURE_VERSION = 'v20160315'
 
 CLOSURE_COMPILER_ARGS = [
   '--compilation_level SIMPLE_OPTIMIZATIONS',
+  '--language_out ECMASCRIPT5_STRICT',
   '--warning_level QUIET'
 ]
 
@@ -131,7 +132,7 @@
 maven_jar(
   name = 'compiler-jar',
   id = 'com.google.javascript:closure-compiler:' + CLOSURE_VERSION,
-  sha1 = '5a2f4be6cf41e27ed7119d26cb8f106300d87d91',
+  sha1 = 'f5b1a03f83a014e545db60a795fcf94db14a5ba2',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [':closure-compiler-externs'],
   visibility = [],
@@ -140,7 +141,7 @@
 maven_jar(
   name = 'closure-compiler-externs',
   id = 'com.google.javascript:closure-compiler-externs:' + CLOSURE_VERSION,
-  sha1 = '7a2dfee8b72cd30ea1a1f652b62f9c0a7aa61fa9',
+  sha1 = 'a0c252a8fced5f0a542302e3f03066c8144d7371',
   license = 'Apache2.0',
   visibility = [],
   attach_source = False,
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs
index f970048..a9db084 100644
--- a/lib/codemirror/cm.defs
+++ b/lib/codemirror/cm.defs
@@ -44,46 +44,119 @@
 # gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java,
 # and in CodeMirror's own mode/meta.js script.
 CM_MODES = [
+  'apl',
+  'asciiarmor',
+  'asn.1',
+  'asterisk',
+  'brainfuck',
   'clike',
   'clojure',
+  'cmake',
+  'cobol',
   'coffeescript',
   'commonlisp',
+  'crystal',
   'css',
+  'cypher',
   'd',
   'dart',
   'diff',
+  'django',
   'dockerfile',
   'dtd',
+  'dylan',
+  'ebnf',
+  'ecl',
+  'eiffel',
+  'elm',
   'erlang',
+  'factor',
+  'fcl',
+  'forth',
+  'fortran',
   'gas',
   'gfm',
+  'gherkin',
   'go',
   'groovy',
+  'haml',
+  'handlebars',
+  'haskell-literate',
   'haskell',
+  'haxe',
+  'htmlembedded',
   'htmlmixed',
+  'http',
+  'idl',
+  'jade',
   'javascript',
+  'jinja2',
+  'jsx',
+  'julia',
+  'livescript',
   'lua',
   'markdown',
+  'mathematica',
+  'mirc',
+  'mllike',
+  'modelica',
+  'mscgen',
+  'mumps',
+  'nginx',
+  'nsis',
+  'ntriples',
+  'octave',
+  'oz',
+  'pascal',
+  'pegjs',
   'perl',
   'php',
   'pig',
   'properties',
+  'protobuf',
   'puppet',
   'python',
+  'q',
   'r',
+  'rpm',
   'rst',
   'ruby',
+  'rust',
+  'sass',
   'scheme',
   'shell',
+  'sieve',
+  'slim',
   'smalltalk',
+  'smarty',
+  'solr',
   'soy',
+  'sparql',
+  'spreadsheet',
   'sql',
   'stex',
+  'stylus',
   'swift',
   'tcl',
+  'textile',
+  'tiddlywiki',
+  'tiki',
+  'toml',
+  'tornado',
+  'troff',
+  'ttcn-cfg',
+  'ttcn',
+  'turtle',
+  'twig',
+  'vb',
+  'vbscript',
   'velocity',
   'verilog',
   'vhdl',
+  'vue',
   'xml',
+  'xquery',
+  'yaml-frontmatter',
   'yaml',
+  'z80',
 ]
diff --git a/lib/fonts/BUCK b/lib/fonts/BUCK
new file mode 100644
index 0000000..c5b78eb
--- /dev/null
+++ b/lib/fonts/BUCK
@@ -0,0 +1,30 @@
+# Source Code Pro. Version 2.010 Roman / 1.030 Italics
+# https://github.com/adobe-fonts/source-code-pro/releases/tag/2.010R-ro%2F1.030R-it
+genrule(
+  name = 'sourcecodepro',
+  cmd = 'zip -rq $OUT .',
+  srcs = [
+    'SourceCodePro-Regular.woff',
+    'SourceCodePro-Regular.woff2'
+  ],
+  out = 'sourcecodepro.zip',
+  license = 'OFL1.1',
+  visibility = ['PUBLIC'],
+)
+
+# Open Sans at Revision 53a5266 and converted using a Google woff file
+# converter (same one that Google Fonts uses).
+# https://github.com/google/fonts/tree/master/apache/opensans
+genrule(
+  name = 'opensans',
+  cmd = 'zip -rq $OUT .',
+  srcs = [
+    'OpenSans-Bold.woff',
+    'OpenSans-Bold.woff2',
+    'OpenSans-Regular.woff',
+    'OpenSans-Regular.woff2'
+  ],
+  out = 'opensans.zip',
+  license = 'Apache2.0',
+  visibility = ['PUBLIC'],
+)
diff --git a/lib/fonts/OpenSans-Bold.woff b/lib/fonts/OpenSans-Bold.woff
new file mode 100644
index 0000000..74c4086
--- /dev/null
+++ b/lib/fonts/OpenSans-Bold.woff
Binary files differ
diff --git a/lib/fonts/OpenSans-Bold.woff2 b/lib/fonts/OpenSans-Bold.woff2
new file mode 100644
index 0000000..44d6c26
--- /dev/null
+++ b/lib/fonts/OpenSans-Bold.woff2
Binary files differ
diff --git a/lib/fonts/OpenSans-Regular.woff b/lib/fonts/OpenSans-Regular.woff
new file mode 100644
index 0000000..882f7c9
--- /dev/null
+++ b/lib/fonts/OpenSans-Regular.woff
Binary files differ
diff --git a/lib/fonts/OpenSans-Regular.woff2 b/lib/fonts/OpenSans-Regular.woff2
new file mode 100644
index 0000000..52217ee
--- /dev/null
+++ b/lib/fonts/OpenSans-Regular.woff2
Binary files differ
diff --git a/lib/fonts/SourceCodePro-Regular.woff b/lib/fonts/SourceCodePro-Regular.woff
new file mode 100644
index 0000000..395436e
--- /dev/null
+++ b/lib/fonts/SourceCodePro-Regular.woff
Binary files differ
diff --git a/lib/fonts/SourceCodePro-Regular.woff2 b/lib/fonts/SourceCodePro-Regular.woff2
new file mode 100644
index 0000000..65cd591
--- /dev/null
+++ b/lib/fonts/SourceCodePro-Regular.woff2
Binary files differ
diff --git a/lib/js/BUCK b/lib/js/BUCK
index b0bdb67..86d69e6 100644
--- a/lib/js/BUCK
+++ b/lib/js/BUCK
@@ -76,6 +76,30 @@
 # version number of the existing bower_component rather than adding a new rule.
 
 bower_component(
+  name = 'accessibility-developer-tools',
+  package = 'accessibility-developer-tools',
+  version = '2.10.0',
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = 'bc1a5e56ff1bed7a7a6ef22a4b4e8300e4822aa5',
+)
+
+bower_component(
+  name = 'async',
+  package = 'async',
+  version = '1.5.2',
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = '1ec975d3b3834646a7e3d4b7e68118b90ed72508',
+)
+
+bower_component(
+  name = 'chai',
+  package = 'chai',
+  version = '3.5.0',
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = '849ad3ee7c77506548b7b5db603a4e150b9431aa',
+)
+
+bower_component(
   name = 'fetch',
   package = 'fetch',
   version = '0.11.0',
@@ -92,6 +116,15 @@
 )
 
 bower_component(
+  name = 'iron-a11y-announcer',
+  package = 'polymerelements/iron-a11y-announcer',
+  version = '1.0.4',
+  deps = [':polymer'],
+  license = 'polymer',
+  sha1 = '9a915711b35092fa2f86ff6e904c4f3e43aa5234',
+)
+
+bower_component(
   name = 'iron-a11y-keys-behavior',
   package = 'polymerelements/iron-a11y-keys-behavior',
   version = '1.1.1',
@@ -142,7 +175,7 @@
 bower_component(
   name = 'iron-dropdown',
   package = 'polymerelements/iron-dropdown',
-  version = '1.2.0',
+  version = '1.3.0',
   deps = [
     ':iron-a11y-keys-behavior',
     ':iron-behaviors',
@@ -152,7 +185,7 @@
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = 'ca97cbfe5873324ba8af80dbdf79af9e72b6f0b8',
+  sha1 = '08ae9c9fa2f2c19a8ab330dfe8240292c8d161cf',
 )
 
 bower_component(
@@ -185,13 +218,14 @@
 bower_component(
   name = 'iron-input',
   package = 'polymerelements/iron-input',
-  version = '1.0.8',
+  version = '1.0.9',
   deps = [
+    ':iron-a11y-announcer',
     ':iron-validatable-behavior',
     ':polymer',
   ],
   license = 'polymer',
-  sha1 = '568c407ffbb524fe2c9ad8230eb895d76c9a8671',
+  sha1 = '4e415c2511ec8ff6c8b17249ec8f02e8d8b1a0d9',
 )
 
 bower_component(
@@ -257,6 +291,22 @@
 )
 
 bower_component(
+  name = 'lodash',
+  package = 'lodash',
+  version = '3.10.1',
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = '2f207a8293c4c554bf6cf071241f7a00dc513d3a',
+)
+
+bower_component(
+  name = 'mocha',
+  package = 'mocha',
+  version = '2.4.5',
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = 'efbb1675710c0ba94a44eb7a4d27040229283197',
+)
+
+bower_component(
   name = 'moment',
   package = 'moment/moment',
   version = '2.12.0',
@@ -272,7 +322,6 @@
     ':iron-meta',
     ':iron-resizable-behavior',
     ':iron-selector',
-    ':paper-styles',
     ':polymer',
     ':web-animations-js',
   ],
@@ -283,9 +332,9 @@
 bower_component(
   name = 'page',
   package = 'visionmedia/page.js',
-  version = '1.6.4',
+  version = '1.7.1',
   license = 'page.js',
-  sha1 = 'cc442386d4e392be26c85873f463db76fafbaeaf',
+  sha1 = '51a05428dd4f68fae1df5f12d0e2b61ba67f7757',
 )
 
 bower_component(
@@ -304,10 +353,10 @@
 bower_component(
   name = 'polymer',
   package = 'polymer/polymer',
-  version = '1.3.1',
+  version = '1.4.0',
   deps = [':webcomponentsjs'],
   license = 'polymer',
-  sha1 = '5f54c14f7b8cecdb356e446a84dabb4ba349d278',
+  sha1 = 'b84725939ead7c7bdf9917b065f68ef8dc790d06',
 )
 
 bower_component(
@@ -320,6 +369,30 @@
 )
 
 bower_component(
+  name = 'sinon-chai',
+  package = 'sinon-chai',
+  version = '2.8.0',
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = '0464b5d944fdf8116bb23e0b02ecfbac945b3517',
+)
+
+bower_component(
+  name = 'sinonjs',
+  package = 'sinonjs',
+  version = '1.17.1',
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = 'a26a6aab7358807de52ba738770f6ac709afd240',
+)
+
+bower_component(
+  name = 'stacky',
+  package = 'stacky',
+  version = '1.3.2',
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = 'd6c07a0112ab2e9677fe085933744466a89232fb',
+)
+
+bower_component(
   name = 'test-fixture',
   package = 'polymerelements/test-fixture',
   version = '1.1.0',
@@ -336,6 +409,25 @@
 )
 
 bower_component(
+  name = 'web-component-tester',
+  package = 'web-component-tester',
+  version = '4.2.2',
+  deps = [
+    ':accessibility-developer-tools',
+    ':async',
+    ':chai',
+    ':lodash',
+    ':mocha',
+    ':sinon-chai',
+    ':sinonjs',
+    ':stacky',
+    ':test-fixture',
+  ],
+  license = 'DO_NOT_DISTRIBUTE',
+  sha1 = '54556000c33d9ed7949aa546c1b4a1531491a5f0',
+)
+
+bower_component(
   name = 'webcomponentsjs',
   package = 'webcomponentsjs',
   version = '0.7.21',
diff --git a/plugins/replication b/plugins/replication
index 77ae05e..8419e92 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 77ae05e49c7413c7c3ae8bcdb73edb7403e38f6a
+Subproject commit 8419e92cb014c1fa5a1f1757e187bf880f99b476
diff --git a/polygerrit-ui/.gitignore b/polygerrit-ui/.gitignore
index 73b5221..b3b74a6 100644
--- a/polygerrit-ui/.gitignore
+++ b/polygerrit-ui/.gitignore
@@ -1,6 +1,6 @@
 node_modules
 npm-debug.log
 dist
-bower.json
+fonts
 bower_components
 .tmp
diff --git a/polygerrit-ui/BUCK b/polygerrit-ui/BUCK
index 4d8144f..fa7f52c 100644
--- a/polygerrit-ui/BUCK
+++ b/polygerrit-ui/BUCK
@@ -16,3 +16,18 @@
     '//lib/js:promise-polyfill',
   ],
 )
+
+genrule(
+  name = 'fonts',
+  cmd = ' && '.join([
+    'cd $TMP',
+    'for file in $SRCS; do unzip -q $file; done',
+    'zip -q $OUT *',
+  ]),
+  srcs = [
+    '//lib/fonts:sourcecodepro',
+    '//lib/fonts:opensans',
+  ],
+  out = 'fonts.zip',
+  visibility = ['PUBLIC'],
+)
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index bef92b6..4ec5411 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -13,7 +13,7 @@
 All other platforms: [download from
 nodejs.org](https://nodejs.org/en/download/).
 
-## Optional: installing [go] (https://golang.org/)
+## Optional: installing [go](https://golang.org/)
 
 This is only required for running the ```run-server.sh``` script for testing. See below.
 
@@ -25,7 +25,7 @@
 brew install go
 ```
 
-All other platforms: [download from golang.org] (https//golang.org/)
+All other platforms: [download from golang.org](https//golang.org/)
 
 # Add [go] to your path
 
@@ -38,7 +38,7 @@
 To test the local UI against gerrit-review.googlesource.com:
 
 ```sh
-./run-server.sh
+./polygerrit-ui/run-server.sh
 ```
 
 Then visit http://localhost:8081
@@ -89,6 +89,14 @@
 WCT_ARGS='-p --some-flag="foo bar"' buck test --no-results-cache --include web
 ```
 
+For interactively working on a single test file, do the following:
+
+```sh
+./polygerrit-ui/run-server.sh
+```
+
+Then visit http://localhost:8081/elements/foo/bar_test.html
+
 ## Style guide
 
 We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
diff --git a/polygerrit-ui/app/BUCK b/polygerrit-ui/app/BUCK
index 391aa97..dfff22f 100644
--- a/polygerrit-ui/app/BUCK
+++ b/polygerrit-ui/app/BUCK
@@ -1,7 +1,8 @@
 include_defs('//lib/js.defs')
 
 WCT_TEST_PATTERNS = [
-  'test/**',
+  'test/*.js',
+  'test/*.html',
   '**/*_test.html',
 ]
 PY_TEST_PATTERNS = ['polygerrit_wct_tests.py']
@@ -10,6 +11,7 @@
   excludes = [
     'BUCK',
     'index.html',
+    'test/**',
   ] + WCT_TEST_PATTERNS + PY_TEST_PATTERNS)
 
 WEBJS = 'bower_components/webcomponentsjs/webcomponents-lite.js'
@@ -24,7 +26,8 @@
   cmd = ' && '.join([
     'mkdir $TMP/polygerrit_ui',
     'cd $TMP/polygerrit_ui',
-    'mkdir -p {elements,bower_components/webcomponentsjs}',
+    'mkdir -p {fonts,elements,bower_components/webcomponentsjs}',
+    'unzip -qd fonts $(location //polygerrit-ui:fonts)',
     'unzip -qd elements $(location :gr-app)',
     'cp -rp $SRCDIR/* .',
     'unzip -p $(location //polygerrit-ui:polygerrit_components) %s>%s' % (WEBJS, WEBJS),
@@ -47,13 +50,13 @@
   components = '//polygerrit-ui:polygerrit_components',
 )
 
-
 bower_components(
   name = 'test_components',
   deps = [
     '//polygerrit-ui:polygerrit_components',
     '//lib/js:iron-test-helpers',
     '//lib/js:test-fixture',
+    '//lib/js:web-component-tester',
   ],
 )
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index caa9674..1d88b28 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -60,6 +60,9 @@
       .u-red {
         color: #D32F2F;
       }
+      .u-gray-background {
+        background-color: #F5F5F5;
+      }
     </style>
     <style include="gr-change-list-styles"></style>
     <span class="cell keyboard">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 1ee49e4..bcceb35 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -72,7 +72,7 @@
 
     _computeLabelTitle: function(change, labelName) {
       var label = change.labels[labelName];
-      if (!label) { return labelName; }
+      if (!label) { return 'Label not applicable'; }
       var significantLabel = label.rejected || label.approved ||
           label.disliked || label.recommended;
       if (significantLabel && significantLabel.name) {
@@ -102,6 +102,8 @@
         if (label.rejected) {
           classes['u-red'] = true;
         }
+      } else {
+        classes['u-gray-background'] = true;
       }
       return Object.keys(classes).sort().join(' ');
     },
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 39dda7f..8447215 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,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="gr-change-list-item.html">
@@ -51,9 +50,10 @@
       assert.equal(element._computeChangeStatusString({status: 'DRAFT'}),
           'Draft');
 
-      assert.equal(element._computeLabelClass({labels: {}}), 'cell label');
+      assert.equal(element._computeLabelClass({labels: {}}),
+          'cell label u-gray-background');
       assert.equal(element._computeLabelClass(
-          {labels: {}}, 'Verified'), 'cell label');
+          {labels: {}}, 'Verified'), 'cell label u-gray-background');
       assert.equal(element._computeLabelClass(
           {labels: {Verified: {approved: true, value: 1}}}, 'Verified'),
           'cell label u-green u-monospace');
@@ -66,13 +66,19 @@
       assert.equal(element._computeLabelClass(
           {labels: {'Code-Review': {value: -1}}}, 'Code-Review'),
           'cell label u-monospace u-red');
+      assert.equal(element._computeLabelClass(
+          {labels: {'Code-Review': {value: -1}}}, 'Verified'),
+          'cell label u-gray-background');
 
       assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'),
-          'Verified');
+          'Label not applicable');
       assert.equal(element._computeLabelTitle(
           {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'),
           'Verified\nby Diffy');
       assert.equal(element._computeLabelTitle(
+          {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Code-Review'),
+          'Label not applicable');
+      assert.equal(element._computeLabelTitle(
           {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'),
           'Verified\nby Diffy');
       assert.equal(element._computeLabelTitle(
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 ca9da5b..25f6d80 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,6 @@
 <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="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 582a28a..60bfc98 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -20,6 +20,7 @@
 
 <link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-request/gr-request.html">
 
@@ -86,6 +87,7 @@
           on-cancel="_handleConfirmDialogCancel"
           hidden></gr-confirm-cherrypick-dialog>
     </gr-overlay>
+    <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   </template>
   <script src="gr-change-actions.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 04b9bb5..608c8f6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -115,10 +115,18 @@
       return '';
     },
 
+    _canSubmitChange: function() {
+      return this.$.jsAPI.canSubmitChange();
+    },
+
     _handleActionTap: function(e) {
       e.preventDefault();
       var el = Polymer.dom(e).rootTarget;
       var key = el.getAttribute('data-action-key');
+      if (key == RevisionActions.SUBMIT &&
+          this._canSubmitChange() === false) {
+        return;
+      }
       var type = el.getAttribute('data-action-type');
       if (type == 'revision') {
         if (key == RevisionActions.REBASE) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 5aba1a0..4e7f393 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -125,6 +125,22 @@
       });
     });
 
+    test('submit change with plugin hook', function(done) {
+      var canSubmitStub = sinon.stub(element, '_canSubmitChange',
+          function() { return false; });
+      var fireRevisionActionStub = sinon.stub(element, '_fireRevisionAction');
+      flush(function() {
+        var submitButton = element.$$('gr-button[data-action-key="submit"]');
+        assert.ok(submitButton);
+        MockInteractions.tap(submitButton);
+        assert.equal(fireRevisionActionStub.callCount, 0);
+
+        canSubmitStub.restore();
+        fireRevisionActionStub.restore();
+        done();
+      });
+    });
+
     test('rebase change', function(done) {
       flush(function() {
         var rebaseButton = element.$$('gr-button[data-action-key="rebase"]');
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 71edb6e..43d9275 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
@@ -53,15 +53,22 @@
         background-color: #ffd4d4;
       }
       @media screen and (max-width: 50em), screen and (min-width: 75em) {
-        section {
-          display: flex;
+        :host {
+          display: table;
         }
-        section:not(:first-of-type) {
-          margin-top: .25em;
+        section {
+          display: table-row;
+        }
+        section:not(:first-of-type) .title,
+        section:not(:first-of-type) .value {
+          padding-top: .5em;
+        }
+        .title,
+        .value {
+          display: table-cell;
         }
         .title {
-          margin-right: .5em;
-          min-width: 9em;
+          padding-right: .5em;
         }
       }
     </style>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 3d2633b..f6733d1 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -28,6 +28,7 @@
     properties: {
       change: Object,
       mutable: Boolean,
+      serverConfig: Object,
     },
 
     behaviors: [
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 f437dbc..6c97b5a 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,7 +24,6 @@
 
 <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 a394b2b..00fb2c9 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,9 +121,9 @@
       .commitMessage {
         font-family: var(--monospace-font-family);
         flex: 0 0 72ch;
-        overflow: auto;
         margin-right: 2em;
         margin-bottom: 1em;
+        overflow-x: hidden;
       }
       .commitMessage h4 {
         font-family: var(--font-family);
@@ -132,12 +132,14 @@
       }
       .commitMessage gr-linked-text {
         --linked-text-white-space: pre;
+        overflow: auto;
       }
       .commitAndRelated {
         align-content: flex-start;
         display: flex;
         flex: 1;
         flex-wrap: wrap;
+        overflow-x: hidden;
       }
       gr-file-list {
         margin-bottom: 1em;
@@ -268,6 +270,7 @@
         <div class="changeInfo-column changeMetadata">
           <gr-change-metadata
               change="[[_change]]"
+              server-config="[[serverConfig]]"
               mutable="[[_loggedIn]]"></gr-change-metadata>
           <gr-change-actions id="actions"
               actions="[[_change.actions]]"
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 af14e06..6d4d4b0 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,6 @@
 <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="../../../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 21263c0..20add83 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,6 @@
 <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="../../../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_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index dc4464b..7302cf2 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,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.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 4f18439..9ef9d02 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,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 43218b7..ad2b925 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -20,86 +20,87 @@
 <link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-request/gr-request.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-reply-dialog">
-  <style>
-    :host {
-      display: block;
-      max-height: 90vh;
-    }
-    :host([disabled]) {
-      pointer-events: none;
-    }
-    :host([disabled]) .container {
-      opacity: .5;
-    }
-    .container {
-      display: flex;
-      flex-direction: column;
-      max-height: 90vh;
-    }
-    section {
-      border-top: 1px solid #ddd;
-      padding: .5em .75em;
-    }
-    .labelsContainer,
-    .actionsContainer {
-      flex-shrink: 0;
-    }
-    .textareaContainer {
-      position: relative;
-      display: flex;
-    }
-    iron-autogrow-textarea {
-      padding: 0;
-      font-family: var(--monospace-font-family);
-    }
-    .message {
-      border: none;
-      width: 100%;
-    }
-    .labelContainer:not(:first-of-type) {
-      margin-top: .5em;
-    }
-    .labelName {
-      display: inline-block;
-      min-width: 7em;
-      margin-right: .5em;
-      white-space: nowrap;
-    }
-    iron-selector {
-      display: inline-flex;
-    }
-    iron-selector > gr-button {
-      margin-right: .25em;
-    }
-    iron-selector > gr-button:first-of-type {
-      border-top-left-radius: 2px;
-      border-bottom-left-radius: 2px;
-    }
-    iron-selector > gr-button:last-of-type {
-      border-top-right-radius: 2px;
-      border-bottom-right-radius: 2px;
-    }
-    iron-selector > gr-button.iron-selected {
-      background-color: #ddd;
-    }
-    .draftsContainer {
-      overflow-y: auto;
-    }
-    .draftsContainer h3 {
-      margin-top: .25em;
-    }
-    .actionsContainer {
-      display: flex;
-      justify-content: space-between;
-    }
-    .action:link,
-    .action:visited {
-      color: #00e;
-    }
-  </style>
   <template>
+    <style>
+      :host {
+        display: block;
+        max-height: 90vh;
+      }
+      :host([disabled]) {
+        pointer-events: none;
+      }
+      :host([disabled]) .container {
+        opacity: .5;
+      }
+      .container {
+        display: flex;
+        flex-direction: column;
+        max-height: 90vh;
+      }
+      section {
+        border-top: 1px solid #ddd;
+        padding: .5em .75em;
+      }
+      .labelsContainer,
+      .actionsContainer {
+        flex-shrink: 0;
+      }
+      .textareaContainer {
+        position: relative;
+        display: flex;
+      }
+      iron-autogrow-textarea {
+        padding: 0;
+        font-family: var(--monospace-font-family);
+      }
+      .message {
+        border: none;
+        width: 100%;
+      }
+      .labelContainer:not(:first-of-type) {
+        margin-top: .5em;
+      }
+      .labelName {
+        display: inline-block;
+        min-width: 7em;
+        margin-right: .5em;
+        white-space: nowrap;
+      }
+      iron-selector {
+        display: inline-flex;
+      }
+      iron-selector > gr-button {
+        margin-right: .25em;
+      }
+      iron-selector > gr-button:first-of-type {
+        border-top-left-radius: 2px;
+        border-bottom-left-radius: 2px;
+      }
+      iron-selector > gr-button:last-of-type {
+        border-top-right-radius: 2px;
+        border-bottom-right-radius: 2px;
+      }
+      iron-selector > gr-button.iron-selected {
+        background-color: #ddd;
+      }
+      .draftsContainer {
+        overflow-y: auto;
+      }
+      .draftsContainer h3 {
+        margin-top: .25em;
+      }
+      .actionsContainer {
+        display: flex;
+        justify-content: space-between;
+      }
+      .action:link,
+      .action:visited {
+        color: #00e;
+      }
+    </style>
     <div class="container">
       <section class="textareaContainer">
         <iron-autogrow-textarea
@@ -139,6 +140,7 @@
         <gr-button class="action cancel" on-tap="_cancelTapHandler">Cancel</gr-button>
       </section>
     </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-reply-dialog.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 6ec4d87..1cd0ce2 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -53,8 +53,8 @@
       Gerrit.RESTClientBehavior,
     ],
 
-    ready: function() {
-      app.accountReady.then(function(account) {
+    attached: function() {
+      this._getAccount().then(function(account) {
         this._account = account;
       }.bind(this));
     },
@@ -115,6 +115,10 @@
       return permittedLabels[label];
     },
 
+    _getAccount: function() {
+      return this.$.restAPI.getAccount();
+    },
+
     _cancelTapHandler: function(e) {
       e.preventDefault();
       this.fire('cancel', null, {bubbles: false});
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 d3072ac..fb2de6a 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,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.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">
@@ -38,6 +37,9 @@
     var server;
 
     setup(function() {
+      stub('gr-reply-dialog', {
+        _getAccount: function() { return Promise.resolve({}); },
+      });
       element = fixture('basic');
       element.changeNum = 42;
       element.patchNum = 1;
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 023f62a..6e081ea 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -17,74 +17,79 @@
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-request/gr-request.html">
 
 <dom-module id="gr-reviewer-list">
-  <style>
-    :host {
-      display: block;
-    }
-    :host([disabled]) {
-      opacity: .8;
-      pointer-events: none;
-    }
-    .autocompleteContainer {
-      position: relative;
-    }
-    .inputContainer {
-      display: flex;
-      margin-top: .25em;
-    }
-    .inputContainer input {
-      flex: 1;
-      font: inherit;
-    }
-    .dropdown {
-      background-color: #fff;
-      box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
-      position: absolute;
-      left: 0;
-      top: 100%;
-      z-index: 1000;
-    }
-    .dropdown .reviewer {
-      cursor: pointer;
-      padding: .5em .75em;
-    }
-    .dropdown .reviewer[selected] {
-      background-color: #ccc;
-    }
-    .remove,
-    .cancel {
-      color: #999;
-    }
-    .remove {
-      font-size: .9em;
-    }
-    .cancel {
-      font-size: 2em;
-      line-height: 1;
-      padding: 0 .15em;
-      text-decoration: none;
-    }
-  </style>
   <template>
+    <style>
+      :host {
+        display: block;
+      }
+      :host([disabled]) {
+        opacity: .8;
+        pointer-events: none;
+      }
+      .autocompleteContainer {
+        position: relative;
+      }
+      .inputContainer {
+        display: flex;
+        margin-top: .25em;
+      }
+      .inputContainer input {
+        flex: 1;
+        font: inherit;
+      }
+      gr-account-chip {
+        margin-top: .3em;
+      }
+      .dropdown {
+        background-color: #fff;
+        box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
+        position: absolute;
+        left: 0;
+        top: 100%;
+        z-index: 1000;
+      }
+      .dropdown .reviewer {
+        cursor: pointer;
+        padding: .5em .75em;
+      }
+      .dropdown .reviewer[selected] {
+        background-color: #ccc;
+      }
+      .remove,
+      .cancel {
+        color: #999;
+      }
+      .remove {
+        font-size: .9em;
+      }
+      .cancel {
+        font-size: 2em;
+        line-height: 1;
+        padding: 0 .15em;
+        text-decoration: none;
+      }
+      @media screen and (max-width: 50em), screen and (min-width: 75em) {
+        gr-account-chip:first-of-type {
+          margin-top: 0;
+        }
+      }
+    </style>
     <gr-ajax id="autocompleteXHR"
         url="[[_computeAutocompleteURL(change)]]"
         params="[[_computeAutocompleteParams(_inputVal)]]"
         on-response="_handleResponse"></gr-ajax>
-
     <template is="dom-repeat" items="[[_reviewers]]" as="reviewer">
-      <div class="reviewer">
-        <gr-account-link account="[[reviewer]]" show-email></gr-account-link>
-        <gr-button link
-            class="remove"
-            data-account-id$="[[reviewer._account_id]]"
-            on-tap="_handleRemoveTap"
-            hidden$="[[!_computeCanRemoveReviewer(reviewer, mutable)]]">remove</gr-buttom>
-      </div>
+      <gr-account-chip class="reviewer" account="[[reviewer]]"
+          on-remove="_handleRemove"
+          data-account-id$="[[reviewer._account_id]]"
+          removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
+      </gr-account-chip>
     </template>
     <div class="controlsContainer" hidden$="[[!mutable]]">
       <div class="autocompleteContainer" hidden$="[[!_showInput]]">
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index 00fc12e..275ab6f 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -161,7 +161,7 @@
       this._autocompleteData = [];
     },
 
-    _handleRemoveTap: function(e) {
+    _handleRemove: function(e) {
       e.preventDefault();
       var target = Polymer.dom(e).rootTarget;
       var accountID = parseInt(target.getAttribute('data-account-id'), 10);
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 898d328..eccc72e 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -175,16 +175,19 @@
         ]
       };
       flushAsynchronousOperations();
-      var removeEls =
-          Polymer.dom(element.root).querySelectorAll('.reviewer > .remove');
-      assert.equal(removeEls.length, 3);
-      Array.from(removeEls).forEach(function(el) {
+      var chips =
+        Polymer.dom(element.root).querySelectorAll('gr-account-chip');
+      assert.equal(chips.length, 3);
+      Array.from(chips).forEach(function(el) {
         var accountID = parseInt(el.getAttribute('data-account-id'), 10);
         assert.ok(accountID);
+
+        var buttonEl = el.$$('gr-button');
+        assert.isNotNull(buttonEl);
         if (accountID == 2) {
-          assert.isTrue(el.hasAttribute('hidden'));
+          assert.isTrue(buttonEl.hasAttribute('hidden'));
         } else {
-          assert.isFalse(el.hasAttribute('hidden'));
+          assert.isFalse(buttonEl.hasAttribute('hidden'));
         }
       });
     });
@@ -240,7 +243,6 @@
 
         element._inputVal = 'andyb';
         server.respond();
-
         element._lastAutocompleteRequest.completes.then(function() {
           assert.isFalse(element.$$('.dropdown').hasAttribute('hidden'));
           var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer');
@@ -257,7 +259,7 @@
             var reviewerEls =
                 Polymer.dom(element.root).querySelectorAll('.reviewer');
             assert.equal(reviewerEls.length, 1);
-            MockInteractions.tap(element.$$('.reviewer > .remove'));
+            MockInteractions.tap(element.$$('.reviewer').$$('gr-button'));
             flushAsynchronousOperations();
             assert.isTrue(element.disabled);
             server.respond();
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 b571d16..2c7550f 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
@@ -20,50 +20,50 @@
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-account-dropdown">
-  <style>
-    :host {
-      display: inline-block;
-    }
-    .dropdown-trigger {
-      text-decoration: none;
-    }
-    .dropdown-content {
-      background-color: #fff;
-      box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
-    }
-    button {
-      background: none;
-      border: none;
-      font: inherit;
-      padding: .3em 0;
-    }
-    gr-avatar {
-      height: 2em;
-      width: 2em;
-      vertical-align: middle;
-    }
-    ul {
-      list-style: none;
-    }
-    ul .accountName {
-      font-weight: bold;
-    }
-    li .accountInfo,
-    li a {
-      display: block;
-      padding: .85em 1em;
-    }
-    li a:link,
-    li a:visited {
-      color: #00e;
-      text-decoration: none;
-    }
-    li a:hover {
-      background-color: #6B82D6;
-      color: #fff;
-    }
-  </style>
   <template>
+    <style>
+      :host {
+        display: inline-block;
+      }
+      .dropdown-trigger {
+        text-decoration: none;
+      }
+      .dropdown-content {
+        background-color: #fff;
+        box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
+      }
+      button {
+        background: none;
+        border: none;
+        font: inherit;
+        padding: .3em 0;
+      }
+      gr-avatar {
+        height: 2em;
+        width: 2em;
+        vertical-align: middle;
+      }
+      ul {
+        list-style: none;
+      }
+      ul .accountName {
+        font-weight: bold;
+      }
+      li .accountInfo,
+      li a {
+        display: block;
+        padding: .85em 1em;
+      }
+      li a:link,
+      li a:visited {
+        color: #00e;
+        text-decoration: none;
+      }
+      li a:hover {
+        background-color: #6B82D6;
+        color: #fff;
+      }
+    </style>
     <gr-button link class="dropdown-trigger" id="trigger"
         on-tap="_showDropdownTapHandler">
       <span hidden$="[[_hasAvatars]]" hidden>[[account.name]]</span>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index a47c50f..9a75fa9 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -194,6 +194,10 @@
             <td><span class="key">a</span></td>
             <td>Review and publish comments</td>
           </tr>
+          <tr>
+            <td><span class="key">,</span></td>
+            <td>Show diff preferences</td>
+          </tr>
         </tbody>
       </table>
     </main>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
new file mode 100644
index 0000000..2971ed2
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -0,0 +1,20 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<script src="../../../bower_components/page/page.js"></script>
+<script src="gr-router.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
new file mode 100644
index 0000000..12fd31a
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -0,0 +1,102 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  // Polymer makes `app` intrinsically defined on the window by virtue of the
+  // custom element having the id "app", but it is made explicit here.
+  var app = document.querySelector('#app');
+  var restAPI = document.createElement('gr-rest-api-interface');
+
+  window.addEventListener('WebComponentsReady', function() {
+    // Middleware
+    page(function(ctx, next) {
+      document.body.scrollTop = 0;
+      next();
+    });
+
+    function loadUser(ctx, next) {
+      restAPI.getLoggedIn().then(function() {
+        next();
+      })
+    }
+
+    // Routes.
+    page('/', loadUser, function(data) {
+      // For backward compatibility with GWT links.
+      if (data.hash) {
+        page.redirect(data.hash);
+        return;
+      }
+      restAPI.getLoggedIn().then(function(loggedIn) {
+        if (loggedIn) {
+          page.redirect('/dashboard/self');
+        } else {
+          page.redirect('/q/status:open');
+        }
+      });
+    });
+
+    page('/dashboard/(.*)', loadUser, function(data) {
+      restAPI.getLoggedIn().then(function(loggedIn) {
+        if (loggedIn) {
+          data.params.view = 'gr-dashboard-view';
+          app.params = data.params;
+        } else {
+          page.redirect('/login/' + encodeURIComponent(data.canonicalPath));
+        }
+      });
+    });
+
+    function queryHandler(data) {
+      data.params.view = 'gr-change-list-view';
+      app.params = data.params;
+    }
+
+    page('/q/:query,:offset', queryHandler);
+    page('/q/:query', queryHandler);
+
+    page(/^\/(\d+)\/?/, function(ctx) {
+      page.redirect('/c/' + ctx.params[0]);
+    });
+
+    page('/c/:changeNum/:patchNum?', function(data) {
+      data.params.view = 'gr-change-view';
+      app.params = data.params;
+    });
+
+    page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, function(ctx) {
+      var params = {
+        changeNum: ctx.params[0],
+        basePatchNum: ctx.params[2],
+        patchNum: ctx.params[4],
+        path: ctx.params[5],
+        view: 'gr-diff-view',
+      };
+      // Don't allow diffing the same patch number against itself because WHY?
+      if (params.basePatchNum == params.patchNum) {
+        page.redirect('/c/' + params.changeNum + '/' + params.patchNum + '/' +
+            params.path);
+        return;
+      }
+      if (!params.patchNum) {
+        params.patchNum = params.basePatchNum;
+        delete(params.basePatchNum);
+      }
+      app.params = params;
+    });
+
+    page.start();
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
index ce7faae..d293060 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -15,6 +15,7 @@
 -->
 
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-diff-comment/gr-diff-comment.html">
 
 <dom-module id="gr-diff-comment-thread">
@@ -35,20 +36,20 @@
       }
     </style>
     <div id="container">
-      <template id="commentList" is="dom-repeat" items="{{_orderedComments}}" as="comment">
+      <template id="commentList" is="dom-repeat" items="[[_orderedComments]]" as="comment">
         <gr-diff-comment
             comment="{{comment}}"
             change-num="[[changeNum]]"
             patch-num="[[patchNum]]"
             draft="[[comment.__draft]]"
-            show-actions="[[showActions]]"
+            show-actions="[[_showActions]]"
             project-config="[[projectConfig]]"
-            on-height-change="_handleCommentHeightChange"
             on-reply="_handleCommentReply"
-            on-discard="_handleCommentDiscard"
+            on-comment-discard="_handleCommentDiscard"
             on-done="_handleCommentDone"></gr-diff-comment>
       </template>
     </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-diff-comment-thread.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index 32c8313..b827d26 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -18,15 +18,9 @@
     is: 'gr-diff-comment-thread',
 
     /**
-     * Fired when the height of the thread changes.
-     *
-     * @event height-change
-     */
-
-    /**
      * Fired when the thread should be discarded.
      *
-     * @event discard
+     * @event thread-discard
      */
 
     properties: {
@@ -37,14 +31,13 @@
       },
       patchNum: String,
       path: String,
-      showActions: Boolean,
       projectConfig: Object,
-
-      _boundWindowResizeHandler: {
-        type: Function,
-        value: function() { return this._handleWindowResize.bind(this); }
+      side: {
+        type: String,
+        value: 'REVISION',
       },
-      _lastHeight: Number,
+
+      _showActions: Boolean,
       _orderedComments: Array,
     },
 
@@ -57,15 +50,25 @@
     ],
 
     attached: function() {
-      window.addEventListener('resize', this._boundWindowResizeHandler);
+      this._getLoggedIn().then(function(loggedIn) {
+        this._showActions = loggedIn;
+      }.bind(this));
     },
 
-    detached: function() {
-      window.removeEventListener('resize', this._boundWindowResizeHandler);
+    addDraft: function(opt_lineNum) {
+      var lastComment = this.comments[this.comments.length - 1];
+      if (lastComment && lastComment.__draft) {
+        var commentEl = this._commentElWithDraftID(
+            lastComment.id || lastComment.__draftID);
+        commentEl.editing = true;
+        return;
+      }
+
+      this.push('comments', this._newDraft(opt_lineNum));
     },
 
-    _handleWindowResize: function(e) {
-      this._heightChanged();
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
     },
 
     _commentsChanged: function(changeRecord) {
@@ -109,11 +112,6 @@
       }
     },
 
-    _handleCommentHeightChange: function(e) {
-      e.stopPropagation();
-      this._heightChanged();
-    },
-
     _handleCommentReply: function(e) {
       var comment = e.detail.comment;
       var quoteStr;
@@ -122,62 +120,62 @@
         var quoteStr = msg.split('\n').map(
             function(line) { return ' > ' + line; }).join('\n') + '\n\n';
       }
-      var reply =
-          this._newReply(comment.id, comment.line, this.path, quoteStr);
+      var reply = this._newReply(comment.id, comment.line, quoteStr);
       this.push('comments', reply);
 
       // Allow the reply to render in the dom-repeat.
       this.async(function() {
         var commentEl = this._commentElWithDraftID(reply.__draftID);
         commentEl.editing = true;
-        this.async(this._heightChanged.bind(this), 1);
       }.bind(this), 1);
     },
 
     _handleCommentDone: function(e) {
       var comment = e.detail.comment;
-      var reply = this._newReply(comment.id, comment.line, this.path, 'Done');
+      var reply = this._newReply(comment.id, comment.line, 'Done');
       this.push('comments', reply);
 
       // Allow the reply to render in the dom-repeat.
       this.async(function() {
         var commentEl = this._commentElWithDraftID(reply.__draftID);
         commentEl.save();
-        this.async(this._heightChanged.bind(this), 1);
       }.bind(this), 1);
     },
 
-    _commentElWithDraftID: function(draftID) {
-      var commentEls =
-          Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
-      for (var i = 0; i < commentEls.length; i++) {
-        if (commentEls[i].comment.__draftID == draftID) {
-          return commentEls[i];
+    _commentElWithDraftID: function(id) {
+      var els = Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
+      for (var i = 0; i < els.length; i++) {
+        if (els[i].comment.id === id || els[i].comment.__draftID === id) {
+          return els[i];
         }
       }
       return null;
     },
 
-    _newReply: function(inReplyTo, line, path, opt_message) {
-      var c = {
+    _newReply: function(inReplyTo, opt_lineNum, opt_message) {
+      var d = this._newDraft(opt_lineNum);
+      d.in_reply_to = inReplyTo;
+      if (opt_message != null) {
+        d.message = opt_message;
+      }
+      return d;
+    },
+
+    _newDraft: function(opt_lineNum) {
+      var d = {
         __draft: true,
         __draftID: Math.random().toString(36),
         __date: new Date(),
-        line: line,
-        path: path,
-        in_reply_to: inReplyTo,
+        path: this.path,
+        side: this.side,
       };
-      if (opt_message != null) {
-        c.message = opt_message;
+      if (opt_lineNum) {
+        d.line = opt_lineNum;
       }
-      return c;
+      return d;
     },
 
     _handleCommentDiscard: function(e) {
-      // TODO(andybons): In Shadow DOM, the event bubbles up, while in Shady
-      // DOM, it respects the bubbles property.
-      // https://github.com/Polymer/polymer/issues/3226
-      e.stopPropagation();
       var diffCommentEl = Polymer.dom(e).rootTarget;
       var idx = this._indexOf(diffCommentEl.comment, this.comments);
       if (idx == -1) {
@@ -186,18 +184,9 @@
       }
       this.splice('comments', idx, 1);
       if (this.comments.length == 0) {
-        this.fire('discard', null, {bubbles: false});
+        this.fire('thread-discard');
         return;
       }
-      this.async(this._heightChanged.bind(this), 1);
-    },
-
-    _heightChanged: function() {
-      var height = this.$.container.offsetHeight;
-      if (height == this._lastHeight) { return; }
-
-      this.fire('height-change', {height: height}, {bubbles: false});
-      this._lastHeight = height;
     },
 
     _indexOf: function(comment, arr) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index 52ad066..82b0faf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -41,6 +41,9 @@
   suite('gr-diff-comment-thread tests', function() {
     var element;
     setup(function() {
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(false); },
+      });
       element = fixture('basic');
     });
 
@@ -124,6 +127,9 @@
     var server;
 
     setup(function() {
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(false); },
+      });
       element = fixture('withComment');
       element.comments = [{
         author: {
@@ -229,7 +235,7 @@
       var draftEl =
           Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1];
       assert.ok(draftEl);
-      draftEl.addEventListener('discard', function() {
+      draftEl.addEventListener('comment-discard', function() {
         server.respond();
         var drafts = element.comments.filter(function(c) {
           return c.__draft == true;
@@ -237,7 +243,7 @@
         assert.equal(drafts.length, 0);
         done();
       });
-      draftEl.fire('discard', null, {bubbles: false});
+      draftEl.fire('comment-discard', null, {bubbles: false});
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index ca6815b..a6a8c4d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -129,7 +129,6 @@
           disabled="{{disabled}}"
           rows="4"
           bind-value="{{_editDraft}}"
-          on-keyup="_handleTextareaKeyup"
           on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea>
       <gr-linked-text class="message"
           pre
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index ca0bedb..81449b8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -18,12 +18,6 @@
     is: 'gr-diff-comment',
 
     /**
-     * Fired when the height of the comment changes.
-     *
-     * @event height-change
-     */
-
-    /**
      * Fired when the Reply action is triggered.
      *
      * @event reply
@@ -38,7 +32,7 @@
     /**
      * Fired when this comment is discarded.
      *
-     * @event discard
+     * @event comment-discard
      */
 
     properties: {
@@ -75,10 +69,6 @@
       this.editing = this._editDraft.length == 0;
     },
 
-    attached: function() {
-      this._heightChanged();
-    },
-
     save: function() {
       this.comment.message = this._editDraft;
       this.disabled = true;
@@ -101,13 +91,6 @@
       }.bind(this));
     },
 
-    _heightChanged: function() {
-      this.async(function() {
-        this.fire('height-change', {height: this.offsetHeight},
-            {bubbles: false});
-      }.bind(this));
-    },
-
     _draftChanged: function(draft) {
       this.$.container.classList.toggle('draft', draft);
     },
@@ -126,7 +109,6 @@
       if (this.comment && this.comment.id) {
         this.$$('.cancel').hidden = !editing;
       }
-      this._heightChanged();
     },
 
     _computeLinkToComment: function(comment) {
@@ -137,12 +119,6 @@
       return draft == null || draft.trim() == '';
     },
 
-    _handleTextareaKeyup: function(e) {
-      // TODO(andybons): This isn't always true, but I can't currently think
-      // of a better metric.
-      this._heightChanged();
-    },
-
     _handleTextareaKeydown: function(e) {
       if (e.keyCode == 27) {  // 'esc'
         this._handleCancel(e);
@@ -190,7 +166,7 @@
     _handleCancel: function(e) {
       this._preventDefaultAndBlur(e);
       if (this.comment.message == null || this.comment.message.length == 0) {
-        this.fire('discard', null, {bubbles: false});
+        this.fire('comment-discard');
         return;
       }
       this._editDraft = this.comment.message;
@@ -205,11 +181,11 @@
       this.disabled = true;
       var commentID = this.comment.id;
       if (!commentID) {
-        this.fire('discard', null, {bubbles: false});
+        this.fire('comment-discard');
         return;
       }
       this._send('DELETE', this._restEndpoint(commentID)).then(function(req) {
-        this.fire('discard', null, {bubbles: false});
+        this.fire('comment-discard');
       }.bind(this)).catch(function(err) {
         alert('Your draft couldn’t be deleted. Check the console and ' +
             'contact the PolyGerrit team for assistance.');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index 799dbf2..c95a9a4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -205,7 +205,7 @@
       assert.isTrue(disabled, 'save button should be disabled.');
 
       var numDiscardEvents = 0;
-      element.addEventListener('discard', function(e) {
+      element.addEventListener('comment-discard', function(e) {
         numDiscardEvents++;
         if (numDiscardEvents == 3) {
           done();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index b945a45..addb026 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -86,14 +86,14 @@
         <input is="iron-input" type="number" id="columnsInput"
             prevent-invalid-input
             allowed-pattern="[0-9]"
-            bind-value="{{prefs.line_length}}">
+            bind-value="{{_newPrefs.line_length}}">
       </div>
       <div class="pref">
         <label for="tabSizeInput">Tab width</label>
         <input is="iron-input" type="number" id="tabSizeInput"
             prevent-invalid-input
             allowed-pattern="[0-9]"
-            bind-value="{{prefs.tab_size}}">
+            bind-value="{{_newPrefs.tab_size}}">
       </div>
       <div class="pref">
         <label for="showTabsInput">Show tabs</label>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
index 70d176e..72805f1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -33,13 +33,14 @@
       prefs: {
         type: Object,
         notify: true,
-        value: function() { return {}; },
       },
       disabled: {
         type: Boolean,
         value: false,
         reflectToAttribute: true,
       },
+
+      _newPrefs: Object,
     },
 
     observers: [
@@ -48,20 +49,25 @@
 
     _prefsChanged: function(changeRecord) {
       var prefs = changeRecord.base;
+      // TODO(andybons): This is not supported in IE. Implement a polyfill.
+      // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
+      // an object as a value, it must be marked enumerable.
+      this._newPrefs = Object.assign({}, prefs);
       this.$.contextSelect.value = prefs.context;
       this.$.showTabsInput.checked = prefs.show_tabs;
     },
 
     _handleContextSelectChange: function(e) {
       var selectEl = Polymer.dom(e).rootTarget;
-      this.set('prefs.context', parseInt(selectEl.value, 10));
+      this.set('_newPrefs.context', parseInt(selectEl.value, 10));
     },
 
     _handleShowTabsTap: function(e) {
-      this.set('prefs.show_tabs', Polymer.dom(e).rootTarget.checked);
+      this.set('_newPrefs.show_tabs', Polymer.dom(e).rootTarget.checked);
     },
 
     _handleSave: function() {
+      this.prefs = this._newPrefs;
       this.fire('save', null, {bubbles: false});
     },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
index 2d86a05..18aeb9a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -45,6 +45,7 @@
         show_tabs: true,
         tab_size: 8,
       };
+      assert.deepEqual(element.prefs, element._newPrefs);
 
       element.$.contextSelect.value = '50';
       element.fire('change', {}, {node: element.$.contextSelect});
@@ -52,10 +53,10 @@
       element.$.tabSizeInput.bindValue = 4;
       MockInteractions.tap(element.$.showTabsInput);
 
-      assert.equal(element.prefs.context, 50);
-      assert.equal(element.prefs.line_length, 80);
-      assert.equal(element.prefs.tab_size, 4);
-      assert.isFalse(element.prefs.show_tabs);
+      assert.equal(element._newPrefs.context, 50);
+      assert.equal(element._newPrefs.line_length, 80);
+      assert.equal(element._newPrefs.tab_size, 4);
+      assert.isFalse(element._newPrefs.show_tabs);
     });
 
     test('events', function(done) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html
deleted file mode 100644
index 972dc2d..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html
+++ /dev/null
@@ -1,97 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
-
-<dom-module id="gr-diff-side">
-  <template>
-    <style>
-      :host,
-      .container {
-        display: flex;
-        flex: 0 0 auto;
-      }
-      .lineNum:before,
-      .code:before {
-        /* To ensure the height is non-zero in these elements, a
-           zero-width space is set as its content. The character
-           itself doesn't matter. Just that there is something
-           there. */
-        content: '\200B';
-      }
-      .lineNum {
-        background-color: #eee;
-        color: #666;
-        padding: 0 .75em;
-        text-align: right;
-      }
-      .canComment .lineNum {
-        cursor: pointer;
-        text-decoration: underline;
-      }
-      .canComment .lineNum:hover {
-        background-color: #ccc;
-      }
-      .lightHighlight {
-        background-color: var(--light-highlight-color);
-      }
-      hl,
-      .darkHighlight {
-        background-color: var(--dark-highlight-color);
-      }
-      .br:after {
-        /* Line feed */
-        content: '\A';
-      }
-      .tab {
-        display: inline-block;
-      }
-      .tab.withIndicator:before {
-        color: #C62828;
-        /* >> character */
-        content: '\00BB';
-      }
-      .numbers,
-      .content {
-        white-space: pre;
-      }
-      .numbers .filler {
-        background-color: #eee;
-      }
-      .contextControl {
-        background-color: #fef;
-      }
-      .contextControl a:link,
-      .contextControl a:visited {
-        display: block;
-        text-decoration: none;
-      }
-      .numbers .contextControl {
-        padding: 0 .75em;
-        text-align: right;
-      }
-      .content .contextControl {
-        text-align: center;
-      }
-    </style>
-    <div class$="[[_computeContainerClass(canComment)]]">
-      <div class="numbers" id="numbers"></div>
-      <div class="content" id="content"></div>
-    </div>
-  </template>
-  <script src="gr-diff-side.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
deleted file mode 100644
index 518da3e..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
+++ /dev/null
@@ -1,613 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function() {
-  'use strict';
-
-  var CharCode = {
-    LESS_THAN: '<'.charCodeAt(0),
-    GREATER_THAN: '>'.charCodeAt(0),
-    AMPERSAND: '&'.charCodeAt(0),
-    SEMICOLON: ';'.charCodeAt(0),
-  };
-
-  var TAB_REGEX = /\t/g;
-
-  Polymer({
-    is: 'gr-diff-side',
-
-    /**
-     * Fired when an expand context control is clicked.
-     *
-     * @event expand-context
-     */
-
-    /**
-     * Fired when a thread's height is changed.
-     *
-     * @event thread-height-change
-     */
-
-    /**
-     * Fired when a draft should be added.
-     *
-     * @event add-draft
-     */
-
-    /**
-     * Fired when a thread is removed.
-     *
-     * @event remove-thread
-     */
-
-    properties: {
-      canComment: {
-        type: Boolean,
-        value: false,
-      },
-      content: {
-        type: Array,
-        notify: true,
-        observer: '_contentChanged',
-      },
-      prefs: {
-        type: Object,
-        value: function() { return {}; },
-      },
-      changeNum: String,
-      patchNum: String,
-      path: String,
-      projectConfig: {
-        type: Object,
-        observer: '_projectConfigChanged',
-      },
-
-      _lineFeedHTML: {
-        type: String,
-        value: '<span class="style-scope gr-diff-side br"></span>',
-        readOnly: true,
-      },
-      _highlightStartTag: {
-        type: String,
-        value: '<hl class="style-scope gr-diff-side">',
-        readOnly: true,
-      },
-      _highlightEndTag: {
-        type: String,
-        value: '</hl>',
-        readOnly: true,
-      },
-      _diffChunkLineNums: {
-        type: Array,
-        value: function() { return []; },
-      },
-      _commentThreadLineNums: {
-        type: Array,
-        value: function() { return []; },
-      },
-      _focusedLineNum: {
-        type: Number,
-        value: 1,
-      },
-    },
-
-    listeners: {
-      'tap': '_tapHandler',
-    },
-
-    observers: [
-      '_prefsChanged(prefs.*)',
-    ],
-
-    rowInserted: function(index) {
-      this.renderLineIndexRange(index, index);
-      this._updateDOMIndices();
-      this._updateJumpIndices();
-    },
-
-    rowRemoved: function(index) {
-      var removedEls = Polymer.dom(this.root).querySelectorAll(
-          '[data-index="' + index + '"]');
-      for (var i = 0; i < removedEls.length; i++) {
-        removedEls[i].parentNode.removeChild(removedEls[i]);
-      }
-      this._updateDOMIndices();
-      this._updateJumpIndices();
-    },
-
-    rowUpdated: function(index) {
-      var removedEls = Polymer.dom(this.root).querySelectorAll(
-          '[data-index="' + index + '"]');
-      for (var i = 0; i < removedEls.length; i++) {
-        removedEls[i].parentNode.removeChild(removedEls[i]);
-      }
-      this.renderLineIndexRange(index, index);
-    },
-
-    scrollToLine: function(lineNum) {
-      if (isNaN(lineNum) || lineNum < 1) { return; }
-
-      var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]');
-      if (!el) { return; }
-
-      // Calculate where the line is relative to the window.
-      var top = el.offsetTop;
-      for (var offsetParent = el.offsetParent;
-           offsetParent;
-           offsetParent = offsetParent.offsetParent) {
-        top += offsetParent.offsetTop;
-      }
-
-      // Scroll the element to the middle of the window. Dividing by a third
-      // instead of half the inner height feels a bit better otherwise the
-      // element appears to be below the center of the window even when it
-      // isn't.
-      window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
-    },
-
-    scrollToNextDiffChunk: function() {
-      this._scrollToNextChunkOrThread(this._diffChunkLineNums);
-    },
-
-    scrollToPreviousDiffChunk: function() {
-      this._scrollToPreviousChunkOrThread(this._diffChunkLineNums);
-    },
-
-    scrollToNextCommentThread: function() {
-      this._scrollToNextChunkOrThread(this._commentThreadLineNums);
-    },
-
-    scrollToPreviousCommentThread: function() {
-      this._scrollToPreviousChunkOrThread(this._commentThreadLineNums);
-    },
-
-    renderLineIndexRange: function(startIndex, endIndex) {
-      this._render(this.content, startIndex, endIndex);
-    },
-
-    hideElementsWithIndex: function(index) {
-      var els = Polymer.dom(this.root).querySelectorAll(
-          '[data-index="' + index + '"]');
-      for (var i = 0; i < els.length; i++) {
-        els[i].setAttribute('hidden', true);
-      }
-    },
-
-    getRowHeight: function(index) {
-      var row = this.content[index];
-      // Filler elements should not be taken into account when determining
-      // height calculations.
-      if (row.type == 'FILLER') {
-        return 0;
-      }
-      if (row.height != null) {
-        return row.height;
-      }
-
-      var selector = '[data-index="' + index + '"]';
-      var els = Polymer.dom(this.root).querySelectorAll(selector);
-      if (els.length != 2) {
-        throw Error('Rows should only consist of two elements');
-      }
-      return Math.max(els[0].offsetHeight, els[1].offsetHeight);
-    },
-
-    getRowNaturalHeight: function(index) {
-      var contentEl = this.$$('.content [data-index="' + index + '"]');
-      return contentEl.naturalHeight || contentEl.offsetHeight;
-    },
-
-    setRowNaturalHeight: function(index) {
-      var lineEl = this.$$('.numbers [data-index="' + index + '"]');
-      var contentEl = this.$$('.content [data-index="' + index + '"]');
-      contentEl.style.height = null;
-      var height = contentEl.offsetHeight;
-      lineEl.style.height = height + 'px';
-      this.content[index].height = height;
-      return height;
-    },
-
-    setRowHeight: function(index, height) {
-      var selector = '[data-index="' + index + '"]';
-      var els = Polymer.dom(this.root).querySelectorAll(selector);
-      for (var i = 0; i < els.length; i++) {
-        els[i].style.height = height + 'px';
-      }
-      this.content[index].height = height;
-    },
-
-    _scrollToNextChunkOrThread: function(lineNums) {
-      for (var i = 0; i < lineNums.length; i++) {
-        if (lineNums[i] > this._focusedLineNum) {
-          this._focusedLineNum = lineNums[i];
-          this.scrollToLine(this._focusedLineNum);
-          return;
-        }
-      }
-    },
-
-    _scrollToPreviousChunkOrThread: function(lineNums) {
-      for (var i = lineNums.length - 1; i >= 0; i--) {
-        if (this._focusedLineNum > lineNums[i]) {
-          this._focusedLineNum = lineNums[i];
-          this.scrollToLine(this._focusedLineNum);
-          return;
-        }
-      }
-    },
-
-    _updateJumpIndices: function() {
-      this._commentThreadLineNums = [];
-      this._diffChunkLineNums = [];
-      var inHighlight = false;
-      for (var i = 0; i < this.content.length; i++) {
-        switch (this.content[i].type) {
-          case 'COMMENT_THREAD':
-            this._commentThreadLineNums.push(
-                this.content[i].comments[0].line);
-            break;
-          case 'CODE':
-            // Only grab the first line of the highlighted chunk.
-            if (!inHighlight && this.content[i].highlight) {
-              this._diffChunkLineNums.push(this.content[i].lineNum);
-              inHighlight = true;
-            } else if (!this.content[i].highlight) {
-              inHighlight = false;
-            }
-            break;
-        }
-      }
-    },
-
-    _updateDOMIndices: function() {
-      // There is no way to select elements with a data-index greater than a
-      // given value. For now, just update all DOM elements.
-      var lineEls = Polymer.dom(this.root).querySelectorAll(
-          '.numbers [data-index]');
-      var contentEls = Polymer.dom(this.root).querySelectorAll(
-          '.content [data-index]');
-      if (lineEls.length != contentEls.length) {
-        throw Error(
-            'There must be the same number of line and content elements');
-      }
-      var index = 0;
-      for (var i = 0; i < this.content.length; i++) {
-        if (this.content[i].hidden) { continue; }
-
-        lineEls[index].setAttribute('data-index', i);
-        contentEls[index].setAttribute('data-index', i);
-        index++;
-      }
-    },
-
-    _prefsChanged: function(changeRecord) {
-      var prefs = changeRecord.base;
-      this.$.content.style.width = prefs.line_length + 'ch';
-    },
-
-    _projectConfigChanged: function(projectConfig) {
-      var threadEls =
-          Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
-      for (var i = 0; i < threadEls.length; i++) {
-        threadEls[i].projectConfig = projectConfig;
-      }
-    },
-
-    _contentChanged: function(diff) {
-      this._clearChildren(this.$.numbers);
-      this._clearChildren(this.$.content);
-      this._render(diff, 0, diff.length - 1);
-      this._updateJumpIndices();
-    },
-
-    _computeContainerClass: function(canComment) {
-      return 'container' + (canComment ? ' canComment' : '');
-    },
-
-    _tapHandler: function(e) {
-      var lineEl = Polymer.dom(e).rootTarget;
-      if (!this.canComment || !lineEl.classList.contains('lineNum')) {
-        return;
-      }
-
-      e.preventDefault();
-      var index = parseInt(lineEl.getAttribute('data-index'), 10);
-      var line = parseInt(lineEl.getAttribute('data-line-num'), 10);
-      this.fire('add-draft', {
-        index: index,
-        line: line
-      }, {bubbles: false});
-    },
-
-    _clearChildren: function(el) {
-      while (el.firstChild) {
-        el.removeChild(el.firstChild);
-      }
-    },
-
-    _handleContextControlClick: function(context, e) {
-      e.preventDefault();
-      this.fire('expand-context', {context: context}, {bubbles: false});
-    },
-
-    _render: function(diff, startIndex, endIndex) {
-      var beforeLineEl;
-      var beforeContentEl;
-      if (endIndex != diff.length - 1) {
-        beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]');
-        beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]');
-        if (!beforeLineEl && !beforeContentEl) {
-          // `endIndex` may be present within the model, but not in the DOM.
-          // Insert it before its successive element.
-          beforeLineEl = this.$$(
-              '.numbers [data-index="' + (endIndex + 1) + '"]');
-          beforeContentEl = this.$$(
-              '.content [data-index="' + (endIndex + 1) + '"]');
-        }
-      }
-
-      for (var i = startIndex; i <= endIndex; i++) {
-        if (diff[i].hidden) { continue; }
-
-        switch (diff[i].type) {
-          case 'CODE':
-            this._renderCode(diff[i], i, beforeLineEl, beforeContentEl);
-            break;
-          case 'FILLER':
-            this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl);
-            break;
-          case 'CONTEXT_CONTROL':
-            this._renderContextControl(diff[i], i, beforeLineEl,
-                beforeContentEl);
-            break;
-          case 'COMMENT_THREAD':
-            this._renderCommentThread(diff[i], i, beforeLineEl,
-                beforeContentEl);
-            break;
-        }
-      }
-    },
-
-    _handleCommentThreadHeightChange: function(e) {
-      var threadEl = Polymer.dom(e).rootTarget;
-      var index = parseInt(threadEl.getAttribute('data-index'), 10);
-      this.content[index].height = e.detail.height;
-      var lineEl = this.$$('.numbers [data-index="' + index + '"]');
-      lineEl.style.height = e.detail.height + 'px';
-      this.fire('thread-height-change', {
-        index: index,
-        height: e.detail.height,
-      }, {bubbles: false});
-    },
-
-    _handleCommentThreadDiscard: function(e) {
-      var threadEl = Polymer.dom(e).rootTarget;
-      var index = parseInt(threadEl.getAttribute('data-index'), 10);
-      this.fire('remove-thread', {index: index}, {bubbles: false});
-    },
-
-    _renderCommentThread: function(thread, index, beforeLineEl,
-        beforeContentEl) {
-      var lineEl = this._createElement('div', 'commentThread');
-      lineEl.classList.add('filler');
-      lineEl.setAttribute('data-index', index);
-      var threadEl = document.createElement('gr-diff-comment-thread');
-      threadEl.addEventListener('height-change',
-          this._handleCommentThreadHeightChange.bind(this));
-      threadEl.addEventListener('discard',
-          this._handleCommentThreadDiscard.bind(this));
-      threadEl.setAttribute('data-index', index);
-      threadEl.changeNum = this.changeNum;
-      threadEl.patchNum = thread.patchNum || this.patchNum;
-      threadEl.path = this.path;
-      threadEl.comments = thread.comments;
-      threadEl.showActions = this.canComment;
-      threadEl.projectConfig = this.projectConfig;
-
-      this.$.numbers.insertBefore(lineEl, beforeLineEl);
-      this.$.content.insertBefore(threadEl, beforeContentEl);
-    },
-
-    _renderContextControl: function(control, index, beforeLineEl,
-        beforeContentEl) {
-      var lineEl = this._createElement('div', 'contextControl');
-      lineEl.setAttribute('data-index', index);
-      lineEl.textContent = '@@';
-      var contentEl = this._createElement('div', 'contextControl');
-      contentEl.setAttribute('data-index', index);
-      var a = this._createElement('a');
-      a.href = '#';
-      a.textContent = 'Show ' + control.numLines + ' common ' +
-          (control.numLines == 1 ? 'line' : 'lines') + '...';
-      a.addEventListener('click',
-          this._handleContextControlClick.bind(this, control));
-      contentEl.appendChild(a);
-
-      this.$.numbers.insertBefore(lineEl, beforeLineEl);
-      this.$.content.insertBefore(contentEl, beforeContentEl);
-    },
-
-    _renderFiller: function(filler, index, beforeLineEl, beforeContentEl) {
-      var lineFillerEl = this._createElement('div', 'filler');
-      lineFillerEl.setAttribute('data-index', index);
-      var fillerEl = this._createElement('div', 'filler');
-      fillerEl.setAttribute('data-index', index);
-      var numLines = filler.numLines || 1;
-
-      lineFillerEl.textContent = '\n'.repeat(numLines);
-      for (var i = 0; i < numLines; i++) {
-        var newlineEl = this._createElement('span', 'br');
-        fillerEl.appendChild(newlineEl);
-      }
-
-      this.$.numbers.insertBefore(lineFillerEl, beforeLineEl);
-      this.$.content.insertBefore(fillerEl, beforeContentEl);
-    },
-
-    _renderCode: function(code, index, beforeLineEl, beforeContentEl) {
-      var lineNumEl = this._createElement('div', 'lineNum');
-      lineNumEl.setAttribute('data-line-num', code.lineNum);
-      lineNumEl.setAttribute('data-index', index);
-      var numLines = code.numLines || 1;
-      lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines);
-
-      var contentEl = this._createElement('div', 'code');
-      contentEl.setAttribute('data-line-num', code.lineNum);
-      contentEl.setAttribute('data-index', index);
-
-      if (code.highlight) {
-        contentEl.classList.add(code.intraline.length > 0 ?
-            'lightHighlight' : 'darkHighlight');
-      }
-
-      var html = util.escapeHTML(code.content);
-      if (code.highlight && code.intraline.length > 0) {
-        html = this._addIntralineHighlights(code.content, html,
-            code.intraline);
-      }
-      if (numLines > 1) {
-        html = this._addNewLines(code.content, html, numLines);
-      }
-      html = this._addTabWrappers(code.content, html);
-
-      // If the html is equivalent to the text then it didn't get highlighted
-      // or escaped. Use textContent which is faster than innerHTML.
-      if (code.content == html) {
-        contentEl.textContent = code.content;
-      } else {
-        contentEl.innerHTML = html;
-      }
-
-      this.$.numbers.insertBefore(lineNumEl, beforeLineEl);
-      this.$.content.insertBefore(contentEl, beforeContentEl);
-    },
-
-    // Advance `index` by the appropriate number of characters that would
-    // represent one source code character and return that index. For
-    // example, for source code '<span>' the escaped html string is
-    // '&lt;span&gt;'. Advancing from index 0 on the prior html string would
-    // return 4, since &lt; maps to one source code character ('<').
-    _advanceChar: function(html, index) {
-      // Any tags don't count as characters
-      while (index < html.length &&
-             html.charCodeAt(index) == CharCode.LESS_THAN) {
-        while (index < html.length &&
-               html.charCodeAt(index) != CharCode.GREATER_THAN) {
-          index++;
-        }
-        index++;  // skip the ">" itself
-      }
-      // An HTML entity (e.g., &lt;) counts as one character.
-      if (index < html.length &&
-          html.charCodeAt(index) == CharCode.AMPERSAND) {
-        while (index < html.length &&
-               html.charCodeAt(index) != CharCode.SEMICOLON) {
-          index++;
-        }
-      }
-      return index + 1;
-    },
-
-    _addIntralineHighlights: function(content, html, highlights) {
-      var startTag = this._highlightStartTag;
-      var endTag = this._highlightEndTag;
-
-      for (var i = 0; i < highlights.length; i++) {
-        var hl = highlights[i];
-
-        var htmlStartIndex = 0;
-        for (var j = 0; j < hl.startIndex; j++) {
-          htmlStartIndex = this._advanceChar(html, htmlStartIndex);
-        }
-
-        var htmlEndIndex = 0;
-        if (hl.endIndex != null) {
-          for (var j = 0; j < hl.endIndex; j++) {
-            htmlEndIndex = this._advanceChar(html, htmlEndIndex);
-          }
-        } else {
-          // If endIndex isn't present, continue to the end of the line.
-          htmlEndIndex = html.length;
-        }
-        // The start and end indices could be the same if a highlight is meant
-        // to start at the end of a line and continue onto the next one.
-        // Ignore it.
-        if (htmlStartIndex != htmlEndIndex) {
-          html = html.slice(0, htmlStartIndex) + startTag +
-                html.slice(htmlStartIndex, htmlEndIndex) + endTag +
-                html.slice(htmlEndIndex);
-        }
-      }
-      return html;
-    },
-
-    _addNewLines: function(content, html, numLines) {
-      var htmlIndex = 0;
-      var indices = [];
-      var numChars = 0;
-      for (var i = 0; i < content.length; i++) {
-        if (numChars > 0 && numChars % this.prefs.line_length == 0) {
-          indices.push(htmlIndex);
-        }
-        htmlIndex = this._advanceChar(html, htmlIndex);
-        if (content[i] == '\t') {
-          numChars += this.prefs.tab_size;
-        } else {
-          numChars++;
-        }
-      }
-      var result = html;
-      var linesLeft = numLines;
-      // Since the result string is being altered in place, start from the end
-      // of the string so that the insertion indices are not affected as the
-      // result string changes.
-      for (var i = indices.length - 1; i >= 0; i--) {
-        result = result.slice(0, indices[i]) + this._lineFeedHTML +
-            result.slice(indices[i]);
-        linesLeft--;
-      }
-      // numLines is the total number of lines this code block should take up.
-      // Fill in the remaining ones.
-      for (var i = 0; i < linesLeft; i++) {
-        result += this._lineFeedHTML;
-      }
-      return result;
-    },
-
-    _addTabWrappers: function(content, html) {
-      // TODO(andybons): CSS tab-size is not supported in IE.
-      // Force this to be a number to prevent arbitrary injection.
-      var tabSize = +this.prefs.tab_size;
-      var htmlStr = '<span class="style-scope gr-diff-side tab ' +
-          (this.prefs.show_tabs ? 'withIndicator" ' : '" ') +
-          'style="tab-size:' + tabSize + ';' +
-          '-moz-tab-size:' + tabSize + ';">\t</span>';
-      return html.replace(TAB_REGEX, htmlStr);
-    },
-
-    _createElement: function(tagName, className) {
-      var el = document.createElement(tagName);
-      // When Shady DOM is being used, these classes are added to account for
-      // Polymer's polyfill behavior. In order to guarantee sufficient
-      // specificity within the CSS rules, these are added to every element.
-      // Since the Polymer DOM utility functions (which would do this
-      // automatically) are not being used for performance reasons, this is
-      // done manually.
-      el.classList.add('style-scope', 'gr-diff-side');
-      if (!!className) {
-        el.classList.add(className);
-      }
-      return el;
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
deleted file mode 100644
index dbae6cb..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
+++ /dev/null
@@ -1,300 +0,0 @@
-<!DOCTYPE html>
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-diff-side</title>
-
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="gr-diff-side.html">
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-side></gr-diff-side>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-diff-side tests', function() {
-    var element;
-
-    function isVisibleInWindow(el) {
-      var rect = el.getBoundingClientRect();
-      return rect.top >= 0 && rect.left >= 0 &&
-          rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
-    }
-
-    setup(function() {
-      element = fixture('basic');
-    });
-
-    test('comments', function() {
-      assert.isFalse(element.$$('.container').classList.contains('canComment'));
-      element.canComment = true;
-      assert.isTrue(element.$$('.container').classList.contains('canComment'));
-      // TODO(andybons): Check for comment creation events firing/not firing
-      // when implemented.
-    });
-
-    test('scroll to line', function() {
-      var content = [];
-      for (var i = 0; i < 300; i++) {
-        content.push({
-          type: 'CODE',
-          content: 'All work and no play makes Jack a dull boy',
-          numLines: 1,
-          lineNum: i + 1,
-          highlight: false,
-          intraline: [],
-        });
-      }
-      element.content = content;
-
-      window.scrollTo(0, 0);
-      element.scrollToLine(-12849);
-      assert.equal(window.scrollY, 0);
-      element.scrollToLine('sup');
-      assert.equal(window.scrollY, 0);
-      var lineEl = element.$$('.numbers .lineNum[data-line-num="150"]');
-      assert.ok(lineEl);
-      element.scrollToLine(150);
-      assert.isAbove(window.scrollY, 0);
-      assert.isTrue(isVisibleInWindow(lineEl), 'element should be visible');
-    });
-
-    test('intraline highlights', function() {
-      var content = '        <gr-linked-text content="' +
-          '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>';
-      var html = util.escapeHTML(content);
-      var highlights = [
-        {startIndex: 0, endIndex: 33},
-        {startIndex: 75},
-      ];
-      assert.equal(
-          content.slice(highlights[0].startIndex, highlights[0].endIndex),
-          '        <gr-linked-text content="');
-      assert.equal(content.slice(highlights[1].startIndex),
-          '"></gr-linked-text>');
-      var result = element._addIntralineHighlights(content, html, highlights);
-      var expected = element._highlightStartTag +
-          '        &lt;gr-linked-text content=&quot;' +
-          element._highlightEndTag +
-          '[[_computeCurrentRevisionMessage(change)]]' +
-          element._highlightStartTag +
-          '&quot;&gt;&lt;&#x2F;gr-linked-text&gt;' +
-          element._highlightEndTag;
-      assert.equal(result, expected);
-    });
-
-    test('newlines', function() {
-      element.prefs = {
-        line_length: 80,
-        tab_size: 4,
-      };
-
-      element.content = [{
-        type: 'CODE',
-        content: 'A'.repeat(50),
-        numLines: 1,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-
-      var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
-      assert.ok(lineEl);
-      var contentEl = element.$$('.content .code[data-line-num="1"]');
-      assert.ok(contentEl);
-      assert.equal(contentEl.innerHTML, 'A'.repeat(50));
-
-      element.content = [{
-        type: 'CODE',
-        content: 'A'.repeat(100),
-        numLines: 2,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-
-      lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
-      assert.ok(lineEl);
-      contentEl = element.$$('.content .code[data-line-num="1"]');
-      assert.ok(contentEl);
-      assert.equal(contentEl.innerHTML,
-          'A'.repeat(80) + element._lineFeedHTML +
-          'A'.repeat(20) + element._lineFeedHTML);
-    });
-
-    test('tabs', function(done) {
-      element.prefs = {
-        line_length: 80,
-        tab_size: 4,
-        show_tabs: true,
-      };
-
-      element.content = [{
-        type: 'CODE',
-        content: 'A'.repeat(50) + '\t' + 'A'.repeat(50),
-        numLines: 2,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-
-      var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
-      assert.ok(lineEl);
-      var contentEl = element.$$('.content .code[data-line-num="1"]');
-      assert.ok(contentEl);
-      var spanEl = contentEl.childNodes[1];
-      assert.equal(spanEl.tagName, 'SPAN');
-      assert.isTrue(spanEl.classList.contains(
-          'style-scope', 'gr-diff-side', 'tab', 'withIndicator'));
-
-      element.prefs.show_tabs = false;
-      element.content = [{
-        type: 'CODE',
-        content: 'A'.repeat(50) + '\t' + 'A'.repeat(50),
-        numLines: 2,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-      contentEl = element.$$('.content .code[data-line-num="1"]');
-      assert.ok(contentEl);
-      spanEl = contentEl.childNodes[1];
-      assert.equal(spanEl.tagName, 'SPAN');
-      assert.isTrue(spanEl.classList.contains(
-          'style-scope', 'gr-diff-side', 'tab'));
-
-      var alertStub = sinon.stub(window, 'alert');
-      element.prefs.tab_size =
-          '"><img src="/" onerror="alert(1);"><span class="';
-      element.content = [{
-        type: 'CODE',
-        content: '\t',
-        numLines: 1,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-      flush(function() {
-        assert.isFalse(alertStub.called);
-        alertStub.restore();
-        done();
-      });
-    });
-
-    test('diff context', function() {
-      var content = [
-        {type: 'CODE', hidden: true, content: '<!DOCTYPE html>'},
-        {type: 'CODE', hidden: true, content: '<meta charset="utf-8">'},
-        {type: 'CODE', hidden: true, content: '<title>My great page</title>'},
-        {type: 'CODE', hidden: true, content: '<style>'},
-        {type: 'CODE', hidden: true, content: '  *,'},
-        {type: 'CODE', hidden: true, content: '  *:before,'},
-        {type: 'CODE', hidden: true, content: '  *:after {'},
-        {type: 'CODE', hidden: true, content: '    box-sizing: border-box;'},
-        {type: 'CONTEXT_CONTROL', numLines: 8, start: 0, end: 8},
-        {type: 'CODE', hidden: false, content: '  }'},
-      ];
-      element.content = content;
-
-      // Only the context elements and the following code line elements should
-      // be present in the DOM.
-      var contextEls =
-          Polymer.dom(element.root).querySelectorAll('.contextControl');
-      assert.equal(contextEls.length, 2);
-      var codeLineEls =
-          Polymer.dom(element.root).querySelectorAll('.lineNum, .code');
-      assert.equal(codeLineEls.length, 2);
-
-      for (var i = 0; i <= 8; i++) {
-        element.content[i].hidden = false;
-      }
-      element.renderLineIndexRange(0, 8);
-      element.hideElementsWithIndex(8);
-
-      contextEls =
-          Polymer.dom(element.root).querySelectorAll('.contextControl');
-      for (var i = 0; i < contextEls.length; i++) {
-        assert.isTrue(contextEls[i].hasAttribute('hidden'));
-      }
-
-      codeLineEls =
-          Polymer.dom(element.root).querySelectorAll('.lineNum, .code');
-
-      // Nine lines should now be present in the DOM.
-      assert.equal(codeLineEls.length, 9 * 2);
-    });
-
-    test('tap line to add a draft', function() {
-      var numAddDraftEvents = 0;
-      sinon.stub(element, 'fire', function(eventName) {
-        if (eventName == 'add-draft') {
-          numAddDraftEvents++;
-        }
-      });
-      element.content = [{type: 'CODE', content: '<!DOCTYPE html>'}];
-      element.canComment = false;
-      flushAsynchronousOperations();
-
-      var lineEl = element.$$('.lineNum');
-      assert.ok(lineEl);
-      MockInteractions.tap(lineEl);
-      assert.equal(numAddDraftEvents, 0);
-
-      element.canComment = true;
-      MockInteractions.tap(lineEl);
-      assert.equal(numAddDraftEvents, 1);
-    });
-
-    test('jump to diff chunk/thread', function() {
-      element.content = [
-        {type: 'CODE', content: '', intraline: [], lineNum: 1, highlight: true},
-        {type: 'CODE', content: '', intraline: [], lineNum: 2, highlight: true},
-        {type: 'CODE', content: '', intraline: [], lineNum: 3 },
-        {type: 'CODE', content: '', intraline: [], lineNum: 4 },
-        {type: 'COMMENT_THREAD', comments: [ { line: 4 }]},
-        {type: 'CODE', content: '', intraline: [], lineNum: 5 },
-        {type: 'CODE', content: '', intraline: [], lineNum: 6, highlight: true},
-        {type: 'CODE', content: '', intraline: [], lineNum: 7, highlight: true},
-        {type: 'CODE', content: '', intraline: [], lineNum: 8 },
-        {type: 'COMMENT_THREAD', comments: [ { line: 8 }]},
-        {type: 'CODE', content: '', intraline: [], lineNum: 9 },
-        {type: 'CODE', content: '', intraline: [], lineNum: 10,
-            highlight: true},
-      ];
-
-      var scrollToLineStub = sinon.stub(element, 'scrollToLine');
-      element.scrollToNextDiffChunk();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(6));
-      element.scrollToPreviousDiffChunk();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(1));
-      element.scrollToNextCommentThread();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(4));
-      element.scrollToNextCommentThread();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(8));
-      element.scrollToPreviousDiffChunk();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(6));
-
-      scrollToLineStub.restore();
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 0dc18ae..7bfa3154 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -21,6 +21,7 @@
 <link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-request/gr-request.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-diff/gr-diff.html">
 
 <dom-module id="gr-diff-view">
@@ -162,13 +163,13 @@
     </h3>
     <gr-diff id="diff"
         change-num="[[_changeNum]]"
-        prefs="{{prefs}}"
         patch-range="[[_patchRange]]"
         path="[[_path]]"
         project-config="[[_projectConfig]]"
         available-patches="[[_computeAvailablePatches(_change.revisions)]]"
         on-render="_handleDiffRender">
     </gr-diff>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-diff-view.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 557c213..456e983 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -26,10 +26,6 @@
      */
 
     properties: {
-      prefs: {
-        type: Object,
-        notify: true,
-      },
       /**
        * URL params passed from the router.
        */
@@ -71,16 +67,14 @@
       Gerrit.RESTClientBehavior,
     ],
 
-    ready: function() {
-      app.accountReady.then(function() {
-        this._loggedIn = app.loggedIn;
-        if (this._loggedIn) {
+    attached: function() {
+      this._getLoggedIn().then(function(loggedIn) {
+        this._loggedIn = loggedIn;
+        if (loggedIn) {
           this._setReviewed(true);
         }
       }.bind(this));
-    },
 
-    attached: function() {
       if (this._path) {
         this.fire('title-change',
             {title: this._computeFileDisplayName(this._path)});
@@ -92,6 +86,10 @@
       window.removeEventListener('resize', this._boundWindowResizeHandler);
     },
 
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
+    },
+
     _handleReviewedChange: function(e) {
       this._setReviewed(Polymer.dom(e).rootTarget.checked);
     },
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 188bc5d..1b102cc 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,6 @@
 <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="../../../test/fake-app.js"></script>
 <script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
@@ -39,6 +38,9 @@
     var server;
 
     setup(function() {
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(false); },
+      });
       element = fixture('basic');
       element.$.changeDetailXHR.auto = false;
       element.$.filesXHR.auto = false;
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
similarity index 73%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-side-by-side.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
index 799aafe..77c790c 100644
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-side-by-side.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
@@ -14,8 +14,8 @@
 (function(window, GrDiffBuilder) {
   'use strict';
 
-  function GrDiffBuilderSideBySide(diff, prefs, outputEl) {
-    GrDiffBuilder.call(this, diff, prefs, outputEl);
+  function GrDiffBuilderSideBySide(diff, comments, prefs, outputEl) {
+    GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
   }
   GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
@@ -35,23 +35,28 @@
   GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine,
       rightLine) {
     var row = this._createElement('tr');
-    this._createPair(section, row, leftLine, leftLine.beforeNumber, 'left');
-    this._createPair(section, row, rightLine, rightLine.afterNumber, 'right');
+    this._appendPair(section, row, leftLine, leftLine.beforeNumber,
+        GrDiffBuilder.Side.LEFT);
+    this._appendPair(section, row, rightLine, rightLine.afterNumber,
+        GrDiffBuilder.Side.RIGHT);
     return row;
   };
 
-  GrDiffBuilderSideBySide.prototype._createPair = function(section, row, line,
+  GrDiffBuilderSideBySide.prototype._appendPair = function(section, row, line,
       lineNumber, side) {
     row.appendChild(this._createLineEl(line, lineNumber, line.type));
     var action = this._createContextControl(section, line);
     if (action) {
       row.appendChild(action);
     } else {
-      var el = this._createTextEl(line);
-      el.classList.add(side);
-      row.appendChild(el);
+      var textEl = this._createTextEl(line);
+      textEl.classList.add(side);
+      var threadEl = this._commentThreadForLine(line, side);
+      if (threadEl) {
+        textEl.appendChild(threadEl);
+      }
+      row.appendChild(textEl);
     }
-    return row;
   };
 
   window.GrDiffBuilderSideBySide = GrDiffBuilderSideBySide;
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
similarity index 83%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-unified.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
index cd48e57..d9517d3 100644
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-unified.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
@@ -14,8 +14,8 @@
 (function(window, GrDiffBuilder) {
   'use strict';
 
-  function GrDiffBuilderUnified(diff, prefs, outputEl) {
-    GrDiffBuilder.call(this, diff, prefs, outputEl);
+  function GrDiffBuilderUnified(diff, comments, prefs, outputEl) {
+    GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
   }
   GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
   GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
@@ -41,7 +41,12 @@
     if (action) {
       row.appendChild(action);
     } else {
-      row.appendChild(this._createTextEl(line));
+      var textEl = this._createTextEl(line);
+      var threadEl = this._commentThreadForLine(line);
+      if (threadEl) {
+        textEl.appendChild(threadEl);
+      }
+      row.appendChild(textEl);
     }
     return row;
   };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
new file mode 100644
index 0000000..f5e7c4c
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
@@ -0,0 +1,580 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the 'License');
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window, GrDiffGroup, GrDiffLine) {
+  'use strict';
+
+  function GrDiffBuilder(diff, comments, prefs, outputEl) {
+    this._comments = comments;
+    this._prefs = prefs;
+    this._outputEl = outputEl;
+    this._groups = [];
+
+    this._commentLocations = this._getCommentLocations(comments);
+    this._processContent(diff.content, this._groups, prefs.context);
+  }
+
+  GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0);
+  GrDiffBuilder.GREATER_THAN_CODE = '>'.charCodeAt(0);
+  GrDiffBuilder.AMPERSAND_CODE = '&'.charCodeAt(0);
+  GrDiffBuilder.SEMICOLON_CODE = ';'.charCodeAt(0);
+
+  GrDiffBuilder.TAB_REGEX = /\t/g;
+
+  GrDiffBuilder.LINE_FEED_HTML =
+      '<span class="style-scope gr-diff br"></span>';
+
+  GrDiffBuilder.GroupType = {
+    ADDED: 'b',
+    BOTH: 'ab',
+    REMOVED: 'a',
+  };
+
+  GrDiffBuilder.Highlights = {
+    ADDED: 'edit_b',
+    REMOVED: 'edit_a',
+  };
+
+  GrDiffBuilder.Side = {
+    LEFT: 'left',
+    RIGHT: 'right',
+  };
+
+  GrDiffBuilder.prototype.emitDiff = function() {
+    for (var i = 0; i < this._groups.length; i++) {
+      this.emitGroup(this._groups[i]);
+    }
+  };
+
+  GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
+    throw Error('Subclasses must implement emitGroup');
+  },
+
+  GrDiffBuilder.prototype._processContent = function(content, groups, context) {
+    this._appendFileComments(groups);
+
+    var WHOLE_FILE = -1;
+    context = content.length > 1 ? context : WHOLE_FILE;
+
+    var lineNums = {
+      left: 0,
+      right: 0,
+    };
+    content = this._splitCommonGroupsWithComments(content, lineNums);
+    for (var i = 0; i < content.length; i++) {
+      var group = content[i];
+      var lines = [];
+
+      if (group[GrDiffBuilder.GroupType.BOTH] !== undefined) {
+        var rows = group[GrDiffBuilder.GroupType.BOTH];
+        this._appendCommonLines(rows, lines, lineNums);
+
+        var hiddenRange = [context, rows.length - context];
+        if (i === 0) {
+          hiddenRange[0] = 0;
+        } else if (i === content.length - 1) {
+          hiddenRange[1] = rows.length;
+        }
+
+        if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 0) {
+          this._insertContextGroups(groups, lines, hiddenRange);
+        } else {
+          groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, lines));
+        }
+        continue;
+      }
+
+      if (group[GrDiffBuilder.GroupType.REMOVED] !== undefined) {
+        var highlights = undefined;
+        if (group[GrDiffBuilder.Highlights.REMOVED] !== undefined) {
+          highlights = this._normalizeIntralineHighlights(
+              group[GrDiffBuilder.GroupType.REMOVED],
+              group[GrDiffBuilder.Highlights.REMOVED]);
+        }
+        this._appendRemovedLines(group[GrDiffBuilder.GroupType.REMOVED], lines,
+            lineNums, highlights);
+      }
+
+      if (group[GrDiffBuilder.GroupType.ADDED] !== undefined) {
+        var highlights = undefined;
+        if (group[GrDiffBuilder.Highlights.ADDED] !== undefined) {
+          highlights = this._normalizeIntralineHighlights(
+            group[GrDiffBuilder.GroupType.ADDED],
+            group[GrDiffBuilder.Highlights.ADDED]);
+        }
+        this._appendAddedLines(group[GrDiffBuilder.GroupType.ADDED], lines,
+            lineNums, highlights);
+      }
+      groups.push(new GrDiffGroup(GrDiffGroup.Type.DELTA, lines));
+    }
+  };
+
+  GrDiffBuilder.prototype._appendFileComments = function(groups) {
+    var line = new GrDiffLine(GrDiffLine.Type.BOTH);
+    line.beforeNumber = GrDiffLine.FILE;
+    line.afterNumber = GrDiffLine.FILE;
+    groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]));
+  };
+
+  GrDiffBuilder.prototype._getCommentLocations = function(comments) {
+    var result = {
+      left: {},
+      right: {},
+    };
+    for (var side in comments) {
+      if (side !== GrDiffBuilder.Side.LEFT &&
+          side !== GrDiffBuilder.Side.RIGHT) {
+        continue;
+      }
+      comments[side].forEach(function(c) {
+        result[side][c.line || GrDiffLine.FILE] = true;
+      });
+    }
+    return result;
+  };
+
+  GrDiffBuilder.prototype._commentIsAtLineNum = function(side, lineNum) {
+    return this._commentLocations[side][lineNum] === true;
+  };
+
+  // In order to show comments out of the bounds of the selected context,
+  // treat them as separate chunks within the model so that the content (and
+  // context surrounding it) renders correctly.
+  GrDiffBuilder.prototype._splitCommonGroupsWithComments = function(content,
+      lineNums) {
+    var result = [];
+    var leftLineNum = lineNums.left;
+    var rightLineNum = lineNums.right;
+    for (var i = 0; i < content.length; i++) {
+      if (!content[i].ab) {
+        result.push(content[i]);
+        if (content[i].a) {
+          leftLineNum += content[i].a.length;
+        }
+        if (content[i].b) {
+          rightLineNum += content[i].b.length;
+        }
+        continue;
+      }
+      var chunk = content[i].ab;
+      var currentChunk = {ab: []};
+      for (var j = 0; j < chunk.length; j++) {
+        leftLineNum++;
+        rightLineNum++;
+        if (this._commentIsAtLineNum(GrDiffBuilder.Side.LEFT, leftLineNum) ||
+            this._commentIsAtLineNum(GrDiffBuilder.Side.RIGHT, rightLineNum)) {
+          if (currentChunk.ab && currentChunk.ab.length > 0) {
+            result.push(currentChunk);
+            currentChunk = {ab: []};
+          }
+          result.push({ab: [chunk[j]]});
+        } else {
+          currentChunk.ab.push(chunk[j]);
+        }
+      }
+      if (currentChunk.ab != null && currentChunk.ab.length > 0) {
+        result.push(currentChunk);
+      }
+    }
+    return result;
+  };
+
+  // The `highlights` array consists of a list of <skip length, mark length>
+  // pairs, where the skip length is the number of characters between the
+  // end of the previous edit and the start of this edit, and the mark
+  // length is the number of edited characters following the skip. The start
+  // of the edits is from the beginning of the related diff content lines.
+  //
+  // Note that the implied newline character at the end of each line is
+  // included in the length calculation, and thus it is possible for the
+  // edits to span newlines.
+  //
+  // A line highlight object consists of three fields:
+  // - contentIndex: The index of the diffChunk `content` field (the line
+  //   being referred to).
+  // - startIndex: Where the highlight should begin.
+  // - endIndex: (optional) Where the highlight should end. If omitted, the
+  //   highlight is meant to be a continuation onto the next line.
+  GrDiffBuilder.prototype._normalizeIntralineHighlights = function(content,
+      highlights) {
+    var contentIndex = 0;
+    var idx = 0;
+    var normalized = [];
+    for (var i = 0; i < highlights.length; i++) {
+      var line = content[contentIndex] + '\n';
+      var hl = highlights[i];
+      var j = 0;
+      while (j < hl[0]) {
+        if (idx === line.length) {
+          idx = 0;
+          line = content[++contentIndex] + '\n';
+          continue;
+        }
+        idx++;
+        j++;
+      }
+      var lineHighlight = {
+        contentIndex: contentIndex,
+        startIndex: idx,
+      };
+
+      j = 0;
+      while (line && j < hl[1]) {
+        if (idx === line.length) {
+          idx = 0;
+          line = content[++contentIndex] + '\n';
+          normalized.push(lineHighlight);
+          lineHighlight = {
+            contentIndex: contentIndex,
+            startIndex: idx,
+          };
+          continue;
+        }
+        idx++;
+        j++;
+      }
+      lineHighlight.endIndex = idx;
+      normalized.push(lineHighlight);
+    }
+    return normalized;
+  };
+
+  GrDiffBuilder.prototype._insertContextGroups = function(groups, lines,
+      hiddenRange) {
+    var linesBeforeCtx = lines.slice(0, hiddenRange[0]);
+    var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
+    var linesAfterCtx = lines.slice(hiddenRange[1]);
+
+    if (linesBeforeCtx.length > 0) {
+      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
+    }
+
+    var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
+    ctxLine.contextLines = hiddenLines;
+    groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
+        [ctxLine]));
+
+    if (linesAfterCtx.length > 0) {
+      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
+    }
+  };
+
+  GrDiffBuilder.prototype._appendCommonLines = function(rows, lines, lineNums) {
+    for (var i = 0; i < rows.length; i++) {
+      var line = new GrDiffLine(GrDiffLine.Type.BOTH);
+      line.text = rows[i];
+      line.beforeNumber = ++lineNums.left;
+      line.afterNumber = ++lineNums.right;
+      lines.push(line);
+    }
+  };
+
+  GrDiffBuilder.prototype._appendRemovedLines = function(rows, lines, lineNums,
+      opt_highlights) {
+    for (var i = 0; i < rows.length; i++) {
+      var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+      line.text = rows[i];
+      line.beforeNumber = ++lineNums.left;
+      if (opt_highlights) {
+        line.highlights = opt_highlights.filter(function(hl) {
+          return hl.contentIndex === i;
+        });
+      }
+      lines.push(line);
+    }
+  };
+
+  GrDiffBuilder.prototype._appendAddedLines = function(rows, lines, lineNums,
+      opt_highlights) {
+    for (var i = 0; i < rows.length; i++) {
+      var line = new GrDiffLine(GrDiffLine.Type.ADD);
+      line.text = rows[i];
+      line.afterNumber = ++lineNums.right;
+      if (opt_highlights) {
+        line.highlights = opt_highlights.filter(function(hl) {
+          return hl.contentIndex === i;
+        });
+      }
+      lines.push(line);
+    }
+  };
+
+  GrDiffBuilder.prototype._createContextControl = function(section, line) {
+    if (!line.contextLines.length) {
+      return null;
+    }
+    var td = this._createElement('td');
+    var button = this._createElement('gr-button', 'showContext');
+    button.setAttribute('link', true);
+    var commonLines = line.contextLines.length;
+    var text = 'Show ' + commonLines + ' common line';
+    if (commonLines > 1) {
+      text += 's';
+    }
+    text += '...';
+    button.textContent = text;
+    button.addEventListener('tap', function(e) {
+      e.detail = {
+        group: new GrDiffGroup(GrDiffGroup.Type.BOTH, line.contextLines),
+        section: section,
+      };
+      // Let it bubble up the DOM tree.
+    });
+    td.appendChild(button);
+    return td;
+  };
+
+  GrDiffBuilder.prototype._getCommentsForLine = function(comments, line,
+      opt_side) {
+    function byLineNum(lineNum) {
+      return function(c) {
+        return (c.line === lineNum) ||
+               (c.line === undefined && lineNum === GrDiffLine.FILE)
+      }
+    }
+    var leftComments =
+        comments[GrDiffBuilder.Side.LEFT].filter(byLineNum(line.beforeNumber));
+    var rightComments =
+        comments[GrDiffBuilder.Side.RIGHT].filter(byLineNum(line.afterNumber));
+
+    var result;
+
+    switch (opt_side) {
+      case GrDiffBuilder.Side.LEFT:
+        result = leftComments;
+        break;
+      case GrDiffBuilder.Side.RIGHT:
+        result = rightComments;
+        break;
+      default:
+        result = leftComments.concat(rightComments);
+        break;
+    }
+
+    return result;
+  };
+
+  GrDiffBuilder.prototype.createCommentThread = function(changeNum, patchNum,
+      path, side, projectConfig) {
+    var threadEl = document.createElement('gr-diff-comment-thread');
+    threadEl.changeNum = changeNum;
+    threadEl.patchNum = patchNum;
+    threadEl.path = path;
+    threadEl.side = side;
+    threadEl.projectConfig = projectConfig;
+    return threadEl;
+  },
+
+  GrDiffBuilder.prototype._commentThreadForLine = function(line, opt_side) {
+    var comments = this._getCommentsForLine(this._comments, line, opt_side);
+    if (!comments || comments.length === 0) {
+      return null;
+    }
+
+    var patchNum = this._comments.meta.patchRange.patchNum;
+    var side = 'REVISION';
+    if (line.type === GrDiffLine.Type.REMOVE ||
+        opt_side === GrDiffBuilder.Side.LEFT) {
+      if (this._comments.meta.patchRange.basePatchNum === 'PARENT') {
+        side = 'PARENT';
+      } else {
+        patchNum = this._comments.meta.patchRange.basePatchNum;
+      }
+    }
+    var threadEl = this.createCommentThread(
+        this._comments.meta.changeNum,
+        patchNum,
+        this._comments.meta.path,
+        side,
+        this._comments.meta.projectConfig);
+    threadEl.comments = comments;
+    return threadEl;
+  };
+
+  GrDiffBuilder.prototype._createLineEl = function(line, number, type) {
+    var td = this._createElement('td');
+    if (line.type === GrDiffLine.Type.BLANK) {
+      return td;
+    } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
+      td.classList.add('contextLineNum');
+      td.setAttribute('data-value', '@@');
+    } else if (line.type === GrDiffLine.Type.BOTH || line.type == type) {
+      td.classList.add('lineNum');
+      td.setAttribute('data-value', number);
+    }
+    return td;
+  };
+
+  GrDiffBuilder.prototype._createTextEl = function(line) {
+    var td = this._createElement('td');
+    if (line.type !== GrDiffLine.Type.BLANK) {
+      td.classList.add('content');
+    }
+    td.classList.add(line.type);
+    var text = line.text;
+    var html = util.escapeHTML(text);
+
+    td.classList.add(line.highlights.length > 0 ?
+        'lightHighlight' : 'darkHighlight');
+
+    if (line.highlights.length > 0) {
+      html = this._addIntralineHighlights(text, html, line.highlights);
+    }
+
+    if (text.length > this._prefs.line_length) {
+      html = this._addNewlines(text, html);
+    }
+    html = this._addTabWrappers(html);
+
+    // If the html is equivalent to the text then it didn't get highlighted
+    // or escaped. Use textContent which is faster than innerHTML.
+    if (html == text) {
+      td.textContent = text;
+    } else {
+      td.innerHTML = html;
+    }
+    return td;
+  };
+
+  // Advance `index` by the appropriate number of characters that would
+  // represent one source code character and return that index. For
+  // example, for source code '<span>' the escaped html string is
+  // '&lt;span&gt;'. Advancing from index 0 on the prior html string would
+  // return 4, since &lt; maps to one source code character ('<').
+  GrDiffBuilder.prototype._advanceChar = function(html, index) {
+    // TODO(andybons): Unicode is all kinds of messed up in JS. Account for it.
+    // https://mathiasbynens.be/notes/javascript-unicode
+
+    // Tags don't count as characters
+    while (index < html.length &&
+           html.charCodeAt(index) == GrDiffBuilder.LESS_THAN_CODE) {
+      while (index < html.length &&
+             html.charCodeAt(index) != GrDiffBuilder.GREATER_THAN_CODE) {
+        index++;
+      }
+      index++;  // skip the ">" itself
+    }
+    // An HTML entity (e.g., &lt;) counts as one character.
+    if (index < html.length &&
+        html.charCodeAt(index) == GrDiffBuilder.AMPERSAND_CODE) {
+      while (index < html.length &&
+             html.charCodeAt(index) != GrDiffBuilder.SEMICOLON_CODE) {
+        index++;
+      }
+    }
+    return index + 1;
+  };
+
+  GrDiffBuilder.prototype._addNewlines = function(text, html) {
+    var htmlIndex = 0;
+    var indices = [];
+    var numChars = 0;
+    for (var i = 0; i < text.length; i++) {
+      if (numChars > 0 && numChars % this._prefs.line_length === 0) {
+        indices.push(htmlIndex);
+      }
+      htmlIndex = this._advanceChar(html, htmlIndex);
+      if (text[i] === '\t') {
+        numChars += this._prefs.tab_size;
+      } else {
+        numChars++;
+      }
+    }
+    var result = html;
+    // Since the result string is being altered in place, start from the end
+    // of the string so that the insertion indices are not affected as the
+    // result string changes.
+    for (var i = indices.length - 1; i >= 0; i--) {
+      result = result.slice(0, indices[i]) + GrDiffBuilder.LINE_FEED_HTML +
+          result.slice(indices[i]);
+    }
+    return result;
+  };
+
+  GrDiffBuilder.prototype._addTabWrappers = function(html) {
+    var htmlStr = this._getTabWrapper(this._prefs.tab_size,
+        this._prefs.show_tabs);
+    return html.replace(GrDiffBuilder.TAB_REGEX, htmlStr);
+  };
+
+  GrDiffBuilder.prototype._addIntralineHighlights = function(content, html,
+      highlights) {
+    var START_TAG = '<hl class="style-scope gr-diff">';
+    var END_TAG = '</hl>';
+
+    for (var i = 0; i < highlights.length; i++) {
+      var hl = highlights[i];
+
+      var htmlStartIndex = 0;
+      // Find the index of the HTML string to insert the start tag.
+      for (var j = 0; j < hl.startIndex; j++) {
+        htmlStartIndex = this._advanceChar(html, htmlStartIndex);
+      }
+
+      var htmlEndIndex = 0;
+      if (hl.endIndex !== undefined) {
+        for (var j = 0; j < hl.endIndex; j++) {
+          htmlEndIndex = this._advanceChar(html, htmlEndIndex);
+        }
+      } else {
+        // If endIndex isn't present, continue to the end of the line.
+        htmlEndIndex = html.length;
+      }
+      // The start and end indices could be the same if a highlight is meant
+      // to start at the end of a line and continue onto the next one.
+      // Ignore it.
+      if (htmlStartIndex !== htmlEndIndex) {
+        html = html.slice(0, htmlStartIndex) + START_TAG +
+              html.slice(htmlStartIndex, htmlEndIndex) + END_TAG +
+              html.slice(htmlEndIndex);
+      }
+    }
+    return html;
+  };
+
+  GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) {
+    // Force this to be a number to prevent arbitrary injection.
+    tabSize = +tabSize;
+    if (isNaN(tabSize)) {
+      throw Error('Invalid tab size from preferences.');
+    }
+
+    var str = '<span class="style-scope gr-diff tab ';
+    if (showTabs) {
+      str += 'withIndicator';
+    }
+    str += '" ';
+    // TODO(andybons): CSS tab-size is not supported in IE.
+    str += 'style="tab-size:' + tabSize + ';';
+    str += 'style="-moz-tab-size:' + tabSize + ';';
+    str += '">\t</span>';
+    return str;
+  };
+
+  GrDiffBuilder.prototype._createElement = function(tagName, className) {
+    var el = document.createElement(tagName);
+    // When Shady DOM is being used, these classes are added to account for
+    // Polymer's polyfill behavior. In order to guarantee sufficient
+    // specificity within the CSS rules, these are added to every element.
+    // Since the Polymer DOM utility functions (which would do this
+    // automatically) are not being used for performance reasons, this is
+    // done manually.
+    el.classList.add('style-scope', 'gr-diff');
+    if (!!className) {
+      el.classList.add(className);
+    }
+    return el;
+  };
+
+  window.GrDiffBuilder = GrDiffBuilder;
+})(window, GrDiffGroup, GrDiffLine);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
new file mode 100644
index 0000000..2e30999
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
@@ -0,0 +1,516 @@
+<!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-diff-builder</title>
+
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="gr-diff-line.js"></script>
+<script src="gr-diff-group.js"></script>
+<script src="gr-diff-builder.js"></script>
+
+<script>
+  suite('gr-diff-builder tests', function() {
+    var builder;
+
+    setup(function() {
+      var prefs = {
+        line_length: 10,
+        show_tabs: true,
+        tab_size: 4,
+      };
+      builder = new GrDiffBuilder({content: []}, {left: [], right: []}, prefs);
+    });
+
+    test('process loaded content', function() {
+      var content = [
+        {
+          ab: [
+            '<!DOCTYPE html>',
+            '<meta charset="utf-8">',
+          ]
+        },
+        {
+          a: [
+            '  Welcome ',
+            '  to the wooorld of tomorrow!',
+          ],
+          b: [
+            '  Hello, world!',
+          ],
+        },
+        {
+          ab: [
+            'Leela: This is the only place the ship can’t hear us, so ',
+            'everyone pretend to shower.',
+            'Fry: Same as every day. Got it.',
+          ]
+        },
+      ];
+      var groups = [];
+
+      builder._processContent(content, groups, -1);
+
+      assert.equal(groups.length, 4);
+
+      var group = groups[0];
+      assert.equal(group.type, GrDiffGroup.Type.BOTH);
+      assert.equal(group.lines.length, 1);
+      assert.equal(group.lines[0].text, '');
+      assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE);
+      assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE);
+
+      group = groups[1];
+      assert.equal(group.type, GrDiffGroup.Type.BOTH);
+      assert.equal(group.lines.length, 2);
+      assert.equal(group.lines.length, 2);
+
+      function beforeNumberFn(l) { return l.beforeNumber; }
+      function afterNumberFn(l) { return l.afterNumber; }
+      function textFn(l) { return l.text; }
+
+      assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
+      assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
+      assert.deepEqual(group.lines.map(textFn), [
+        '<!DOCTYPE html>',
+        '<meta charset="utf-8">',
+      ]);
+
+      group = groups[2];
+      assert.equal(group.type, GrDiffGroup.Type.DELTA);
+      assert.equal(group.lines.length, 3);
+      assert.equal(group.adds.length, 1);
+      assert.equal(group.removes.length, 2);
+      assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
+      assert.deepEqual(group.adds.map(afterNumberFn), [3]);
+      assert.deepEqual(group.removes.map(textFn), [
+        '  Welcome ',
+        '  to the wooorld of tomorrow!',
+      ]);
+      assert.deepEqual(group.adds.map(textFn), [
+        '  Hello, world!',
+      ]);
+
+      group = groups[3];
+      assert.equal(group.type, GrDiffGroup.Type.BOTH);
+      assert.equal(group.lines.length, 3);
+      assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
+      assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
+      assert.deepEqual(group.lines.map(textFn), [
+        'Leela: This is the only place the ship can’t hear us, so ',
+        'everyone pretend to shower.',
+        'Fry: Same as every day. Got it.',
+      ]);
+    });
+
+    test('insert context groups', function() {
+      var content = [
+        {ab: []},
+        {a: ['all work and no play make andybons a dull boy']},
+        {ab: []},
+        {b: ['elgoog elgoog elgoog']},
+        {ab: []},
+      ];
+      for (var i = 0; i < 100; i++) {
+        content[0].ab.push('all work and no play make jack a dull boy');
+        content[4].ab.push('all work and no play make jill a dull girl');
+      }
+      for (var i = 0; i < 5; i++) {
+        content[2].ab.push('no tv and no beer make homer go crazy');
+      }
+      var groups = [];
+      var context = 10;
+
+      builder._processContent(content, groups, context);
+
+      assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
+      assert.equal(groups[0].lines.length, 1);
+      assert.equal(groups[0].lines[0].text, '');
+      assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
+      assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
+
+      assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+      assert.equal(groups[1].lines[0].contextLines.length, 90);
+      groups[1].lines[0].contextLines.forEach(function(l) {
+        assert.equal(l.text, content[0].ab[0]);
+      });
+
+      assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+      assert.equal(groups[2].lines.length, context);
+      groups[2].lines.forEach(function(l) {
+        assert.equal(l.text, content[0].ab[0]);
+      });
+
+      assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
+      assert.equal(groups[3].lines.length, 1);
+      assert.equal(groups[3].removes.length, 1);
+      assert.equal(groups[3].removes[0].text,
+          'all work and no play make andybons a dull boy');
+
+      assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+      assert.equal(groups[4].lines.length, 5);
+      groups[4].lines.forEach(function(l) {
+        assert.equal(l.text, content[2].ab[0]);
+      });
+
+      assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
+      assert.equal(groups[5].lines.length, 1);
+      assert.equal(groups[5].adds.length, 1);
+      assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
+
+      assert.equal(groups[6].type, GrDiffGroup.Type.BOTH);
+      assert.equal(groups[6].lines.length, context);
+      groups[6].lines.forEach(function(l) {
+        assert.equal(l.text, content[4].ab[0]);
+      });
+
+      assert.equal(groups[7].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+      assert.equal(groups[7].lines[0].contextLines.length, 90);
+      groups[7].lines[0].contextLines.forEach(function(l) {
+        assert.equal(l.text, content[4].ab[0]);
+      });
+
+      content = [
+        {a: ['all work and no play make andybons a dull boy']},
+        {ab: []},
+        {b: ['elgoog elgoog elgoog']},
+      ];
+      for (var i = 0; i < 50; i++) {
+        content[1].ab.push('no tv and no beer make homer go crazy');
+      }
+      groups = [];
+
+      builder._processContent(content, groups, 10);
+
+      assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
+      assert.equal(groups[0].lines.length, 1);
+      assert.equal(groups[0].lines[0].text, '');
+      assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
+      assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
+
+      assert.equal(groups[1].type, GrDiffGroup.Type.DELTA);
+      assert.equal(groups[1].lines.length, 1);
+      assert.equal(groups[1].removes.length, 1);
+      assert.equal(groups[1].removes[0].text,
+          'all work and no play make andybons a dull boy');
+
+      assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+      assert.equal(groups[2].lines.length, context);
+      groups[2].lines.forEach(function(l) {
+        assert.equal(l.text, content[1].ab[0]);
+      });
+
+      assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+      assert.equal(groups[3].lines[0].contextLines.length, 30);
+      groups[3].lines[0].contextLines.forEach(function(l) {
+        assert.equal(l.text, content[1].ab[0]);
+      });
+
+      assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+      assert.equal(groups[4].lines.length, context);
+      groups[4].lines.forEach(function(l) {
+        assert.equal(l.text, content[1].ab[0]);
+      });
+
+      assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
+      assert.equal(groups[5].lines.length, 1);
+      assert.equal(groups[5].adds.length, 1);
+      assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
+    });
+
+    test('newlines', function() {
+      var text = 'abcdef';
+      assert.equal(builder._addNewlines(text, text), text);
+      text = 'a'.repeat(20);
+      assert.equal(builder._addNewlines(text, text),
+          'a'.repeat(10) +
+          GrDiffBuilder.LINE_FEED_HTML +
+          'a'.repeat(10));
+
+      text = '<span class="thumbsup">👍</span>';
+      var html = '&lt;span class=&quot;thumbsup&quot;&gt;👍&lt;&#x2F;span&gt;';
+      assert.equal(builder._addNewlines(text, html),
+          '&lt;span clas' +
+          GrDiffBuilder.LINE_FEED_HTML +
+          's=&quot;thumbsu' +
+          GrDiffBuilder.LINE_FEED_HTML +
+          'p&quot;&gt;👍&lt;&#x2F;spa' +
+          GrDiffBuilder.LINE_FEED_HTML +
+          'n&gt;');
+
+      text = '01234\t56789';
+      assert.equal(builder._addNewlines(text, text),
+          '01234\t5' +
+          GrDiffBuilder.LINE_FEED_HTML +
+          '6789');
+    });
+
+    test('tab wrapper insertion', function() {
+      var html = 'abc\tdef';
+      var wrapper = builder._getTabWrapper(
+          builder._prefs.tab_size,
+          builder._prefs.show_tabs);
+      assert.ok(wrapper);
+      assert.isAbove(wrapper.length, 0);
+      assert.equal(builder._addTabWrappers(html), 'abc' + wrapper + 'def');
+      assert.throws(builder._getTabWrapper.bind(
+          builder,
+          '"><img src="/" onerror="alert(1);"><span class="',
+          true));
+    });
+
+    test('comments', function() {
+      var line = new GrDiffLine(GrDiffLine.Type.BOTH);
+      line.beforeNumber = 3;
+      line.afterNumber = 5;
+
+      var comments = {left: [], right:[]};
+      assert.deepEqual(builder._getCommentsForLine(comments, line), []);
+      assert.deepEqual(builder._getCommentsForLine(comments, line,
+          GrDiffBuilder.Side.LEFT), []);
+      assert.deepEqual(builder._getCommentsForLine(comments, line,
+          GrDiffBuilder.Side.RIGHT), []);
+
+      comments = {
+        left: [
+          {id: 'l3', line: 3},
+          {id: 'l5', line: 5},
+        ],
+        right: [
+          {id: 'r3', line: 3},
+          {id: 'r5', line: 5},
+        ],
+      };
+      assert.deepEqual(builder._getCommentsForLine(comments, line),
+          [{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
+      assert.deepEqual(builder._getCommentsForLine(comments, line,
+          GrDiffBuilder.Side.LEFT), [{id: 'l3', line: 3}]);
+      assert.deepEqual(builder._getCommentsForLine(comments, line,
+          GrDiffBuilder.Side.RIGHT), [{id: 'r5', line: 5}]);
+    });
+
+    test('comment thread creation', function() {
+      builder._comments = {
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: '3',
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'l3', line: 3},
+          {id: 'l5', line: 5},
+        ],
+        right: [
+          {id: 'r5', line: 5},
+        ],
+      };
+
+      function checkThreadProps(patchNum, side, comments) {
+        assert.equal(threadEl.changeNum, '42');
+        assert.equal(threadEl.patchNum, patchNum);
+        assert.equal(threadEl.path, '/path/to/foo');
+        assert.equal(threadEl.side, side);
+        assert.deepEqual(threadEl.projectConfig, {foo: 'bar'});
+        assert.deepEqual(threadEl.comments, comments);
+      }
+
+      var line = new GrDiffLine(GrDiffLine.Type.BOTH);
+      line.beforeNumber = 5;
+      line.afterNumber = 5;
+      threadEl = builder._commentThreadForLine(line);
+      checkThreadProps('3', 'REVISION',
+          [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
+
+      threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
+      checkThreadProps('3', 'REVISION', [{id: 'r5', line: 5}]);
+
+      threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
+      checkThreadProps('3', 'PARENT', [{id: 'l5', line: 5}]);
+
+      builder._comments.meta.patchRange.basePatchNum = '1';
+
+      threadEl = builder._commentThreadForLine(line);
+      checkThreadProps('3', 'REVISION',
+          [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
+
+      threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
+      checkThreadProps('1', 'REVISION', [{id: 'l5', line: 5}]);
+
+      threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
+      checkThreadProps('3', 'REVISION', [{id: 'r5', line: 5}]);
+
+      builder._comments.meta.patchRange.basePatchNum = 'PARENT';
+
+      line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+      line.beforeNumber = 5;
+      line.afterNumber = 5;
+      threadEl = builder._commentThreadForLine(line);
+      checkThreadProps('3', 'PARENT',
+          [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
+
+      line = new GrDiffLine(GrDiffLine.Type.ADD);
+      line.beforeNumber = 3;
+      line.afterNumber = 5;
+      threadEl = builder._commentThreadForLine(line);
+      checkThreadProps('3', 'REVISION',
+          [{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
+    });
+
+    test('break up common diff chunks', function() {
+      builder._commentLocations = {
+        left: {1: true},
+        right: {10: true},
+      };
+      var lineNums = {
+        left: 0,
+        right: 0,
+      };
+      var content = [
+        {
+          ab: [
+            'Copyright (C) 2015 The Android Open Source Project',
+            '',
+            'Licensed under the Apache License, Version 2.0 (the "License");',
+            'you may not use this file except in compliance with the License.',
+            'You may obtain a copy of the License at',
+            '',
+            'http://www.apache.org/licenses/LICENSE-2.0',
+            '',
+            'Unless required by applicable law or agreed to in writing, ',
+            'software distributed under the License is distributed on an ',
+            '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ',
+            'either express or implied. See the License for the specific ',
+            'language governing permissions and limitations under the License.',
+          ]
+        }
+      ];
+      var result = builder._splitCommonGroupsWithComments(content, lineNums);
+      assert.deepEqual(result, [
+        {
+          ab: ['Copyright (C) 2015 The Android Open Source Project'],
+        },
+        {
+          ab: [
+            '',
+            '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, ',
+          ]
+        },
+        {
+          ab: ['software distributed under the License is distributed on an '],
+        },
+        {
+          ab: [
+            '"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.',
+          ]
+        }
+      ]);
+    });
+
+    test('intraline normalization', function() {
+      // The content and highlights are in the format returned by the Gerrit
+      // REST API.
+      var content = [
+        '      <section class="summary">',
+        '        <gr-linked-text content="' +
+            '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
+        '      </section>',
+      ];
+      var highlights = [
+        [31, 34], [42, 26]
+      ];
+      var results = GrDiffBuilder.prototype._normalizeIntralineHighlights(
+          content, highlights);
+      assert.deepEqual(results, [
+        {
+          contentIndex: 0,
+          startIndex: 31,
+        },
+        {
+          contentIndex: 1,
+          startIndex: 0,
+          endIndex: 33,
+        },
+        {
+          contentIndex: 1,
+          startIndex: 75,
+        },
+        {
+          contentIndex: 2,
+          startIndex: 0,
+          endIndex: 6,
+        }
+      ]);
+
+      content = [
+        '        this._path = value.path;',
+        '',
+        '        // When navigating away from the page, there is a possibility that the',
+        '        // patch number is no longer a part of the URL (say when navigating to',
+        '        // the top-level change info view) and therefore undefined in `params`.',
+        '        if (!this._patchRange.patchNum) {',
+      ];
+      highlights = [
+        [14, 17],
+        [11, 70],
+        [12, 67],
+        [12, 67],
+        [14, 29],
+      ];
+      results = GrDiffBuilder.prototype._normalizeIntralineHighlights(content,
+          highlights);
+      assert.deepEqual(results, [
+        {
+          contentIndex: 0,
+          startIndex: 14,
+          endIndex: 31,
+        },
+        {
+          contentIndex: 2,
+          startIndex: 8,
+          endIndex: 78,
+        },
+        {
+          contentIndex: 3,
+          startIndex: 11,
+          endIndex: 78,
+        },
+        {
+          contentIndex: 4,
+          startIndex: 11,
+          endIndex: 78,
+        },
+        {
+          contentIndex: 5,
+          startIndex: 12,
+          endIndex: 41,
+        }
+      ]);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-group.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-group_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
similarity index 95%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-line.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
index 1dd81ed..ea00a3d 100644
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-line.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
@@ -17,6 +17,7 @@
   function GrDiffLine(type) {
     this.type = type;
     this.contextLines = [];
+    this.highlights = [];
   }
 
   GrDiffLine.prototype.beforeNumber = 0;
@@ -33,6 +34,8 @@
     REMOVE: 'remove',
   };
 
+  GrDiffLine.FILE = 'FILE';
+
   GrDiffLine.BLANK_LINE = new GrDiffLine(GrDiffLine.Type.BLANK);
 
   window.GrDiffLine = GrDiffLine;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index a6d92a2..f32454d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -20,13 +20,19 @@
 <link rel="import" href="../../shared/gr-request/gr-request.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
+<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
 <link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
-<link rel="import" href="../gr-diff-side/gr-diff-side.html">
 <link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
 
 <dom-module id="gr-diff">
   <template>
     <style>
+      :host {
+        --light-remove-highlight-color: #fee;
+        --dark-remove-highlight-color: #ffd4d4;
+        --light-add-highlight-color: #efe;
+        --dark-add-highlight-color: #d4ffd4;
+      }
       .loading {
         padding: 0 var(--default-horizontal-margin) 1em;
         color: #666;
@@ -45,16 +51,99 @@
         display: flex;
         font: 12px var(--monospace-font-family);
         overflow-x: auto;
+        will-change: transform;
       }
-      gr-diff-side:first-of-type {
-        --light-highlight-color: #fee;
-        --dark-highlight-color: #ffd4d4;
-      }
-      gr-diff-side:last-of-type {
-        --light-highlight-color: #efe;
-        --dark-highlight-color: #d4ffd4;
+      table {
+        border-collapse: collapse;
         border-right: 1px solid #ddd;
       }
+      .section {
+        background-color: #eee;
+      }
+      .blank,
+      .content {
+        background-color: #fff;
+      }
+      .lineNum,
+      .content {
+        vertical-align: top;
+        white-space: pre;
+      }
+      .contextLineNum:before,
+      .lineNum:before {
+        display: inline-block;
+        color: #666;
+        content: attr(data-value);
+        padding: 0 .75em;
+        text-align: right;
+        width: 100%;
+      }
+      .canComment .lineNum[data-value] {
+        cursor: pointer;
+      }
+      .canComment .lineNum[data-value]:before {
+        text-decoration: underline;
+      }
+      .canComment .lineNum[data-value]:hover:before {
+        background-color: #ccc;
+      }
+      .canComment .lineNum[data-value="FILE"]:before {
+        content: 'File';
+      }
+      .content {
+        overflow: hidden;
+        width: var(--content-width, 80ch);
+      }
+      .content.left {
+        -webkit-user-select: var(--left-user-select, text);
+        -moz-user-select: var(--left-user-select, text);
+        -ms-user-select: var(--left-user-select, text);
+        user-select: var(--left-user-select, text);
+      }
+      .content.right {
+        -webkit-user-select: var(--right-user-select, text);
+        -moz-user-select: var(--right-user-select, text);
+        -ms-user-select: var(--right-user-select, text);
+        user-select: var(--right-user-select, text);
+      }
+      .content.add hl,
+      .content.add.darkHighlight {
+        background-color: var(--dark-add-highlight-color);
+      }
+      .content.add.lightHighlight {
+        background-color: var(--light-add-highlight-color);
+      }
+      .content.remove hl,
+      .content.remove.darkHighlight {
+        background-color: var(--dark-remove-highlight-color);
+      }
+      .content.remove.lightHighlight {
+        background-color: var(--light-remove-highlight-color);
+      }
+      .contextControl {
+        color: #849;
+        background-color: #fef;
+      }
+      .contextControl gr-button {
+        display: block;
+        font-family: var(--monospace-font-family);
+        text-decoration: none;
+      }
+      .contextControl td:not(.lineNum) {
+        text-align: center;
+      }
+      .br:after {
+        /* Line feed */
+        content: '\A';
+      }
+      .tab {
+        display: inline-block;
+      }
+      .tab.withIndicator:before {
+        color: #C62828;
+        /* >> character */
+        content: '\00BB';
+      }
     </style>
     <div class="loading" hidden$="[[!_loading]]">Loading...</div>
     <div hidden$="[[_loading]]" hidden>
@@ -67,44 +156,29 @@
         <gr-button link
            class="prefsButton"
            on-tap="_handlePrefsTap"
-           hidden$="[[!prefs]]"
+           hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]"
            hidden>Diff View Preferences</gr-button>
       </div>
       <gr-overlay id="prefsOverlay" with-backdrop>
         <gr-diff-preferences
-            prefs="{{prefs}}"
+            prefs="{{_prefs}}"
             on-save="_handlePrefsSave"
             on-cancel="_handlePrefsCancel"></gr-diff-preferences>
       </gr-overlay>
 
-      <div class="diffContainer">
-        <gr-diff-side id="leftDiff"
-            change-num="[[changeNum]]"
-            patch-num="[[patchRange.basePatchNum]]"
-            path="[[path]]"
-            content="{{_diff.leftSide}}"
-            prefs="[[prefs]]"
-            can-comment="[[_loggedIn]]"
-            project-config="[[projectConfig]]"
-            on-expand-context="_handleExpandContext"
-            on-thread-height-change="_handleThreadHeightChange"
-            on-add-draft="_handleAddDraft"
-            on-remove-thread="_handleRemoveThread"></gr-diff-side>
-        <gr-diff-side id="rightDiff"
-            change-num="[[changeNum]]"
-            patch-num="[[patchRange.patchNum]]"
-            path="[[path]]"
-            content="{{_diff.rightSide}}"
-            prefs="[[prefs]]"
-            can-comment="[[_loggedIn]]"
-            project-config="[[projectConfig]]"
-            on-expand-context="_handleExpandContext"
-            on-thread-height-change="_handleThreadHeightChange"
-            on-add-draft="_handleAddDraft"
-            on-remove-thread="_handleRemoveThread"></gr-diff-side>
+      <div class$="[[_computeContainerClass(_loggedIn, _viewMode)]]"
+          on-tap="_handleTap"
+          on-mousedown="_handleMouseDown"
+          on-copy="_handleCopy">
+        <table id="diffTable"></table>
       </div>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
+  <script src="gr-diff-line.js"></script>
+  <script src="gr-diff-group.js"></script>
+  <script src="gr-diff-builder.js"></script>
+  <script src="gr-diff-builder-side-by-side.js"></script>
+  <script src="gr-diff-builder-unified.js"></script>
   <script src="gr-diff.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index cac1f3e..12ceacb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -14,6 +14,16 @@
 (function() {
   'use strict';
 
+  var DiffViewMode = {
+    SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+    UNIFIED: 'UNIFIED_DIFF',
+  };
+
+  var DiffSide = {
+    LEFT: 'left',
+    RIGHT: 'right',
+  };
+
   Polymer({
     is: 'gr-diff',
 
@@ -26,148 +36,379 @@
     properties: {
       availablePatches: Array,
       changeNum: String,
-      /*
-       * A single object to encompass basePatchNum and patchNum is used
-       * so that both can be set at once without incremental observers
-       * firing after each property changes.
-       */
       patchRange: Object,
       path: String,
-      prefs: {
-        type: Object,
-        notify: true,
-      },
-      projectConfig: Object,
 
-      _prefsReady: {
+      projectConfig: {
         type: Object,
-        readOnly: true,
-        value: function() {
-          return new Promise(function(resolve) {
-            this._resolvePrefsReady = resolve;
-          }.bind(this));
-        },
+        observer: '_projectConfigChanged',
       },
-      _baseComments: Array,
-      _comments: Array,
-      _drafts: Array,
-      _baseDrafts: Array,
-      /**
-       * Base (left side) comments and drafts grouped by line number.
-       * Only used for initial rendering.
-       */
-      _groupedBaseComments: {
-        type: Object,
-        value: function() { return {}; },
-      },
-      /**
-       * Comments and drafts (right side) grouped by line number.
-       * Only used for initial rendering.
-       */
-      _groupedComments: {
-        type: Object,
-        value: function() { return {}; },
-      },
-      _diffResponse: Object,
-      _diff: {
-        type: Object,
-        value: function() { return {}; },
-      },
+
       _loggedIn: {
         type: Boolean,
         value: false,
       },
-      _initialRenderComplete: {
-        type: Boolean,
-        value: false,
-      },
       _loading: {
         type: Boolean,
         value: true,
       },
-      _savedPrefs: Object,
-
-      _diffPreferencesPromise: Object,  // Used for testing.
+      _viewMode: {
+        type: String,
+        value: DiffViewMode.SIDE_BY_SIDE,
+      },
+      _diff: Object,
+      _diffBuilder: Object,
+      _prefs: Object,
+      _selectionSide: {
+        type: String,
+        observer: '_selectionSideChanged',
+      },
+      _comments: Object,
+      _focusedSection: {
+        type: Number,
+        value: -1,
+      },
+      _focusedThread: {
+        type: Number,
+        value: -1,
+      },
     },
 
     observers: [
-      '_prefsChanged(prefs.*)',
+      '_prefsChanged(_prefs.*)',
     ],
 
-    ready: function() {
-      app.accountReady.then(function() {
-        this._loggedIn = app.loggedIn;
+    attached: function() {
+      this._getLoggedIn().then(function(loggedIn) {
+        this._loggedIn = loggedIn;
       }.bind(this));
-    },
 
-    scrollToLine: function(lineNum) {
-      // TODO(andybons): Should this always be the right side?
-      this.$.rightDiff.scrollToLine(lineNum);
-    },
-
-    scrollToNextDiffChunk: function() {
-      this.$.rightDiff.scrollToNextDiffChunk();
-    },
-
-    scrollToPreviousDiffChunk: function() {
-      this.$.rightDiff.scrollToPreviousDiffChunk();
-    },
-
-    scrollToNextCommentThread: function() {
-      this.$.rightDiff.scrollToNextCommentThread();
-    },
-
-    scrollToPreviousCommentThread: function() {
-      this.$.rightDiff.scrollToPreviousCommentThread();
+      this.addEventListener('thread-discard',
+          this._handleThreadDiscard.bind(this));
+      this.addEventListener('comment-discard',
+          this._handleCommentDiscard.bind(this));
     },
 
     reload: function() {
+      this._clearDiffContent();
       this._loading = true;
-      // If a diff takes a considerable amount of time to render, the previous
-      // diff can end up showing up while the DOM is constructed. Clear the
-      // content on a reload to prevent this.
-      this._diff = {
-        leftSide: [],
-        rightSide: [],
-      };
 
-      var diffLoaded = this._getDiff().then(function(diff) {
-        this._diffResponse = diff;
+      var promises = [];
+
+      promises.push(this._getDiff().then(function(diff) {
+        this._diff = diff;
+        this._loading = false;
+      }.bind(this)));
+
+      promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
+        this._comments = comments;
+      }.bind(this)));
+
+      promises.push(this._getDiffPreferences().then(function(prefs) {
+        this._prefs = prefs;
+      }.bind(this)));
+
+      return Promise.all(promises).then(function() {
+        this._render();
       }.bind(this));
+    },
 
-      var promises = [
-        this._prefsReady,
-        diffLoaded,
-      ];
+    showDiffPreferences: function() {
+      this.$.prefsOverlay.open();
+    },
 
-      return app.accountReady.then(function() {
-        promises.push(this._getDiffComments().then(function(res) {
-          this._baseComments = res.baseComments;
-          this._comments = res.comments;
-        }.bind(this)));
+    scrollToLine: function(lineNum) {
+      if (isNaN(lineNum) || lineNum < 1) { return; }
 
-        if (!app.loggedIn) {
-          this._baseDrafts = [];
-          this._drafts = [];
-        } else {
-          promises.push(this._getDiffDrafts().then(function(res) {
-            this._baseDrafts = res.baseComments;
-            this._drafts = res.comments;
-          }.bind(this)));
-        }
+      var lineEls = Polymer.dom(this.root).querySelectorAll(
+          '.lineNum[data-value="' + lineNum + '"]');
 
-        return Promise.all(promises).then(function() {
-          this._render();
-          this._loading = false;
-        }.bind(this)).catch(function(err) {
-          this._loading = false;
-          alert('Oops. Something went wrong. Check the console and bug the ' +
+      // Always choose the right side.
+      var el = lineEls.length === 2 ? lineEls[1] : lineEls[0];
+      this._scrollToElement(el);
+    },
+
+    scrollToNextDiffChunk: function() {
+      this._focusedSection = this._advanceElementWithinNodeList(
+          this._getDeltaSections(), this._focusedSection, 1);
+    },
+
+    scrollToPreviousDiffChunk: function() {
+      this._focusedSection = this._advanceElementWithinNodeList(
+          this._getDeltaSections(), this._focusedSection, -1);
+    },
+
+    scrollToNextCommentThread: function() {
+      this._focusedThread = this._advanceElementWithinNodeList(
+          this._getCommentThreads(), this._focusedThread, 1);
+    },
+
+    scrollToPreviousCommentThread: function() {
+      this._focusedThread = this._advanceElementWithinNodeList(
+          this._getCommentThreads(), this._focusedThread, -1);
+    },
+
+    _advanceElementWithinNodeList: function(els, curIndex, direction) {
+      var idx = Math.max(0, Math.min(els.length - 1, curIndex + direction));
+      if (curIndex !== idx) {
+        this._scrollToElement(els[idx]);
+        return idx;
+      }
+      return curIndex;
+    },
+
+    _getCommentThreads: function() {
+      return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
+    },
+
+    _getDeltaSections: function() {
+      return Polymer.dom(this.root).querySelectorAll('.section.delta');
+    },
+
+    _scrollToElement: function(el) {
+      if (!el) { return; }
+
+      // Calculate where the element is relative to the window.
+      var top = el.offsetTop;
+      for (var offsetParent = el.offsetParent;
+           offsetParent;
+           offsetParent = offsetParent.offsetParent) {
+        top += offsetParent.offsetTop;
+      }
+
+      // Scroll the element to the middle of the window. Dividing by a third
+      // instead of half the inner height feels a bit better otherwise the
+      // element appears to be below the center of the window even when it
+      // isn't.
+      window.scrollTo(0, top - (window.innerHeight / 3) +
+          (el.offsetHeight / 2));
+    },
+
+    _computeContainerClass: function(loggedIn, viewMode) {
+      var classes = ['diffContainer'];
+      switch (viewMode) {
+        case DiffViewMode.UNIFIED:
+          classes.push('unified');
+          break;
+        case DiffViewMode.SIDE_BY_SIDE:
+          classes.push('sideBySide');
+          break
+        default:
+          throw Error('Invalid view mode: ', viewMode);
+      }
+      if (loggedIn) {
+        classes.push('canComment');
+      }
+      return classes.join(' ');
+    },
+
+    _computePrefsButtonHidden: function(prefs, loggedIn) {
+      return !loggedIn || !prefs;
+    },
+
+    _handlePrefsTap: function(e) {
+      e.preventDefault();
+      this.$.prefsOverlay.open();
+    },
+
+    _handlePrefsSave: function(e) {
+      e.stopPropagation();
+      var el = Polymer.dom(e).rootTarget;
+      el.disabled = true;
+      this._saveDiffPreferences().then(function() {
+        this.$.prefsOverlay.close();
+        el.disabled = false;
+      }.bind(this)).catch(function(err) {
+        el.disabled = false;
+        alert('Oops. Something went wrong. Check the console and bug the ' +
               'PolyGerrit team for assistance.');
-          throw err;
-        }.bind(this));
+        throw err;
+      });
+    },
+
+    _saveDiffPreferences: function() {
+      return this.$.restAPI.saveDiffPreferences(this._prefs);
+    },
+
+    _handlePrefsCancel: function(e) {
+      e.stopPropagation();
+      this.$.prefsOverlay.close();
+    },
+
+    _handleTap: function(e) {
+      var el = Polymer.dom(e).rootTarget;
+
+      if (el.classList.contains('showContext')) {
+        this._showContext(e.detail.group, e.detail.section);
+      } else if (el.classList.contains('lineNum')) {
+        this._handleLineTap(el);
+      }
+    },
+
+    _handleLineTap: function(el) {
+      this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) { return; }
+
+        var value = el.getAttribute('data-value');
+        if (value === GrDiffLine.FILE) {
+          this._addDraft(el);
+          return;
+        }
+        var lineNum = parseInt(value, 10);
+        if (isNaN(lineNum)) {
+          throw Error('Invalid line number: ' + value);
+        }
+        this._addDraft(el, lineNum);
       }.bind(this));
     },
 
+    _addDraft: function(lineEl, opt_lineNum) {
+      var threadEl;
+
+      // Does a thread already exist at this line?
+      var contentEl = lineEl.nextSibling;
+      while (contentEl && !contentEl.classList.contains('content')) {
+        contentEl = contentEl.nextSibling;
+      }
+      if (contentEl.childNodes.length > 0 &&
+          contentEl.lastChild.nodeName === 'GR-DIFF-COMMENT-THREAD') {
+        threadEl = contentEl.lastChild;
+      } else {
+        var patchNum = this.patchRange.patchNum;
+        var side = 'REVISION';
+        if (contentEl.classList.contains(DiffSide.LEFT) ||
+            contentEl.classList.contains('remove')) {
+          if (this.patchRange.basePatchNum === 'PARENT') {
+            side = 'PARENT';
+          } else {
+            patchNum = this.patchRange.basePatchNum;
+          }
+        }
+        threadEl = this._builder.createCommentThread(this.changeNum, patchNum,
+            this.path, side, this.projectConfig);
+        contentEl.appendChild(threadEl);
+      }
+      threadEl.addDraft(opt_lineNum);
+    },
+
+    _handleThreadDiscard: function(e) {
+      var el = Polymer.dom(e).rootTarget;
+      el.parentNode.removeChild(el);
+    },
+
+    _handleCommentDiscard: function(e) {
+      var comment = Polymer.dom(e).rootTarget.comment;
+      this._removeComment(comment);
+    },
+
+    _removeComment: function(comment) {
+      if (!comment.id) { return; }
+      this._removeCommentFromSide(comment, DiffSide.LEFT) ||
+          this._removeCommentFromSide(comment, DiffSide.RIGHT);
+    },
+
+    _removeCommentFromSide: function(comment, side) {
+      var idx = -1;
+      for (var i = 0; i < this._comments[side].length; i++) {
+        if (this._comments[side][i].id === comment.id) {
+          idx = i;
+          break;
+        }
+      }
+      if (idx !== -1) {
+        this.splice('_comments.' + side, idx, 1);
+        return true;
+      }
+      return false;
+    },
+
+    _handleMouseDown: function(e) {
+      var el = Polymer.dom(e).rootTarget;
+      var side;
+      for (var node = el; node != null; node = node.parentNode) {
+        if (!node.classList) { continue; }
+
+        if (node.classList.contains(DiffSide.LEFT)) {
+          side = DiffSide.LEFT;
+          break;
+        } else if (node.classList.contains(DiffSide.RIGHT)) {
+          side = DiffSide.RIGHT;
+          break;
+        }
+      }
+      this._selectionSide = side;
+    },
+
+    _selectionSideChanged: function(side) {
+      if (side) {
+        var oppositeSide = side === DiffSide.RIGHT ?
+            DiffSide.LEFT : DiffSide.RIGHT;
+        this.customStyle['--' + side + '-user-select'] = 'text';
+        this.customStyle['--' + oppositeSide + '-user-select'] = 'none';
+      } else {
+        this.customStyle['--left-user-select'] = 'text';
+        this.customStyle['--right-user-select'] = 'text';
+      }
+      this.updateStyles();
+    },
+
+    _handleCopy: function(e) {
+      var text = this._getSelectedText(this._selectionSide);
+      e.clipboardData.setData('Text', text);
+      e.preventDefault();
+    },
+
+    _getSelectedText: function(opt_side) {
+      var sel = window.getSelection();
+      var range = sel.getRangeAt(0);
+      var doc = range.cloneContents();
+      var selector = '.content';
+      if (opt_side) {
+        selector += '.' + opt_side;
+      }
+      var contentEls = Polymer.dom(doc).querySelectorAll(selector);
+
+      if (contentEls.length === 0) {
+        return doc.textContent;
+      }
+
+      var text = '';
+      for (var i = 0; i < contentEls.length; i++) {
+        text += contentEls[i].textContent + '\n';
+      }
+      return text;
+    },
+
+    _showContext: function(group, sectionEl) {
+      this._builder.emitGroup(group, sectionEl);
+      sectionEl.parentNode.removeChild(sectionEl);
+    },
+
+    _prefsChanged: function(prefsChangeRecord) {
+      var prefs = prefsChangeRecord.base;
+      this.customStyle['--content-width'] = prefs.line_length + 'ch';
+      this.updateStyles();
+
+      if (this._diff && this._comments) {
+        this._render();
+      }
+    },
+
+    _render: function() {
+      this._clearDiffContent();
+      this._builder = this._getDiffBuilder(this._diff, this._comments,
+          this._prefs);
+      this._builder.emitDiff(this._diff.content);
+
+      this.async(function() {
+        this.fire('render', null, {bubbles: false});
+      }.bind(this), 1);
+    },
+
+    _clearDiffContent: function() {
+      this.$.diffTable.innerHTML = null;
+    },
+
     _getDiff: function() {
       return this.$.restAPI.getDiff(
           this.changeNum,
@@ -185,528 +426,95 @@
     },
 
     _getDiffDrafts: function() {
-      return this.$.restAPI.getDiffDrafts(
-          this.changeNum,
-          this.patchRange.basePatchNum,
-          this.patchRange.patchNum,
-          this.path);
-    },
-
-    showDiffPreferences: function() {
-      this.$.prefsOverlay.open();
-    },
-
-    _prefsChanged: function(changeRecord) {
-      if (this._initialRenderComplete) {
-        this._render();
-      }
-      this._resolvePrefsReady(changeRecord.base);
-    },
-
-    _render: function() {
-      this._groupCommentsAndDrafts();
-      this._processContent();
-
-      // Allow for the initial rendering to complete before firing the event.
-      this.async(function() {
-        this.fire('render', null, {bubbles: false});
-      }.bind(this), 1);
-
-      this._initialRenderComplete = true;
-    },
-
-    _handlePrefsTap: function(e) {
-      e.preventDefault();
-
-      // TODO(andybons): This is not supported in IE. Implement a polyfill.
-      // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
-      // an object as a value, it must be marked enumerable.
-      this._savedPrefs = Object.assign({}, this.prefs);
-      this.$.prefsOverlay.open();
-    },
-
-    _handlePrefsSave: function(e) {
-      e.stopPropagation();
-      var el = Polymer.dom(e).rootTarget;
-      el.disabled = true;
-      app.accountReady.then(function() {
-        if (!this._loggedIn) {
-          el.disabled = false;
-          this.$.prefsOverlay.close();
-          return;
+      return this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) {
+          return Promise.resolve({baseComments: [], comments: []});
         }
-        this._saveDiffPreferences().then(function() {
-          this.$.prefsOverlay.close();
-          el.disabled = false;
-        }.bind(this)).catch(function(err) {
-          el.disabled = false;
-          alert('Oops. Something went wrong. Check the console and bug the ' +
-                'PolyGerrit team for assistance.');
-          throw err;
-        });
+        return this.$.restAPI.getDiffDrafts(
+            this.changeNum,
+            this.patchRange.basePatchNum,
+            this.patchRange.patchNum,
+            this.path);
       }.bind(this));
     },
 
-    _saveDiffPreferences: function() {
-      var xhr = document.createElement('gr-request');
-      this._diffPreferencesPromise = xhr.send({
-        method: 'PUT',
-        url: '/accounts/self/preferences.diff',
-        body: this.prefs,
-      });
-      return this._diffPreferencesPromise;
+    _getDiffCommentsAndDrafts: function() {
+      var promises = [];
+      promises.push(this._getDiffComments());
+      promises.push(this._getDiffDrafts());
+      return Promise.all(promises).then(function(results) {
+        return Promise.resolve({
+          comments: results[0],
+          drafts: results[1],
+        });
+      }).then(this._normalizeDiffCommentsAndDrafts.bind(this));
     },
 
-    _handlePrefsCancel: function(e) {
-      e.stopPropagation();
-      this.prefs = this._savedPrefs;
-      this.$.prefsOverlay.close();
-    },
-
-    _handleExpandContext: function(e) {
-      var ctx = e.detail.context;
-      var contextControlIndex = -1;
-      for (var i = ctx.start; i <= ctx.end; i++) {
-        this._diff.leftSide[i].hidden = false;
-        this._diff.rightSide[i].hidden = false;
-        if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' &&
-            this._diff.rightSide[i].type == 'CONTEXT_CONTROL') {
-          contextControlIndex = i;
+    _getDiffPreferences: function() {
+      return this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) {
+          // These defaults should match the defaults in
+          // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
+          // NOTE: There are some settings that don't apply to PolyGerrit
+          // (Render mode being at least one of them).
+          return Promise.resolve({
+            auto_hide_diff_table_header: true,
+            context: 10,
+            cursor_blink_rate: 0,
+            ignore_whitespace: 'IGNORE_NONE',
+            intraline_difference: true,
+            line_length: 100,
+            show_line_endings: true,
+            show_tabs: true,
+            show_whitespace_errors: true,
+            syntax_highlighting: true,
+            tab_size: 8,
+            theme: 'DEFAULT',
+          });
         }
-      }
-      this._diff.leftSide[contextControlIndex].hidden = true;
-      this._diff.rightSide[contextControlIndex].hidden = true;
-
-      this.$.leftDiff.hideElementsWithIndex(contextControlIndex);
-      this.$.rightDiff.hideElementsWithIndex(contextControlIndex);
-
-      this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end);
-      this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end);
+        return this.$.restAPI.getDiffPreferences();
+      }.bind(this));
     },
 
-    _handleThreadHeightChange: function(e) {
-      var index = e.detail.index;
-      var diffEl = Polymer.dom(e).rootTarget;
-      var otherSide = diffEl == this.$.leftDiff ?
-          this.$.rightDiff : this.$.leftDiff;
-
-      var threadHeight = e.detail.height;
-      var otherSideHeight;
-      if (otherSide.content[index].type == 'COMMENT_THREAD') {
-        otherSideHeight = otherSide.getRowNaturalHeight(index);
-      } else {
-        otherSideHeight = otherSide.getRowHeight(index);
+    _normalizeDiffCommentsAndDrafts: function(results) {
+      function markAsDraft(d) {
+        d.__draft = true;
+        return d;
       }
-      var maxHeight = Math.max(threadHeight, otherSideHeight);
-      this.$.leftDiff.setRowHeight(index, maxHeight);
-      this.$.rightDiff.setRowHeight(index, maxHeight);
-    },
-
-    _handleAddDraft: function(e) {
-      var insertIndex = e.detail.index + 1;
-      var diffEl = Polymer.dom(e).rootTarget;
-      var content = diffEl.content;
-      if (content[insertIndex] &&
-          content[insertIndex].type == 'COMMENT_THREAD') {
-        // A thread is already here. Do nothing.
-        return;
-      }
-      var comment = {
-        type: 'COMMENT_THREAD',
-        comments: [{
-          __draft: true,
-          __draftID: Math.random().toString(36),
-          line: e.detail.line,
+      var baseDrafts = results.drafts.baseComments.map(markAsDraft);
+      var drafts = results.drafts.comments.map(markAsDraft);
+      return Promise.resolve({
+        meta: {
           path: this.path,
-        }]
-      };
-      if (diffEl == this.$.leftDiff &&
-          this.patchRange.basePatchNum == 'PARENT') {
-        comment.comments[0].side = 'PARENT';
-        comment.patchNum = this.patchRange.patchNum;
-      }
-
-      if (content[insertIndex] &&
-          content[insertIndex].type == 'FILLER') {
-        content[insertIndex] = comment;
-        diffEl.rowUpdated(insertIndex);
-      } else {
-        content.splice(insertIndex, 0, comment);
-        diffEl.rowInserted(insertIndex);
-      }
-
-      var otherSide = diffEl == this.$.leftDiff ?
-          this.$.rightDiff : this.$.leftDiff;
-      if (otherSide.content[insertIndex] == null ||
-          otherSide.content[insertIndex].type != 'COMMENT_THREAD') {
-        otherSide.content.splice(insertIndex, 0, {
-          type: 'FILLER',
-        });
-        otherSide.rowInserted(insertIndex);
-      }
-    },
-
-    _handleRemoveThread: function(e) {
-      var diffEl = Polymer.dom(e).rootTarget;
-      var otherSide = diffEl == this.$.leftDiff ?
-          this.$.rightDiff : this.$.leftDiff;
-      var index = e.detail.index;
-
-      if (otherSide.content[index].type == 'FILLER') {
-        otherSide.content.splice(index, 1);
-        otherSide.rowRemoved(index);
-        diffEl.content.splice(index, 1);
-        diffEl.rowRemoved(index);
-      } else if (otherSide.content[index].type == 'COMMENT_THREAD') {
-        diffEl.content[index] = {type: 'FILLER'};
-        diffEl.rowUpdated(index);
-        var height = otherSide.setRowNaturalHeight(index);
-        diffEl.setRowHeight(index, height);
-      } else {
-        throw Error('A thread cannot be opposite anything but filler or ' +
-            'another thread');
-      }
-    },
-
-    _processContent: function() {
-      var leftSide = [];
-      var rightSide = [];
-      var initialLineNum = 0 + (this._diffResponse.content.skip || 0);
-      var ctx = {
-        hidingLines: false,
-        lastNumLinesHidden: 0,
-        left: {
-          lineNum: initialLineNum,
+          changeNum: this.changeNum,
+          patchRange: this.patchRange,
+          projectConfig: this.projectConfig,
         },
-        right: {
-          lineNum: initialLineNum,
-        }
-      };
-      var content = this._breakUpCommonChunksWithComments(ctx,
-          this._diffResponse.content);
-      var context = this.prefs.context;
-      if (context == -1) {
-        // Show the entire file.
-        context = Infinity;
-      }
-      for (var i = 0; i < content.length; i++) {
-        if (i == 0) {
-          ctx.skipRange = [0, context];
-        } else if (i == content.length - 1) {
-          ctx.skipRange = [context, 0];
-        } else {
-          ctx.skipRange = [context, context];
-        }
-        ctx.diffChunkIndex = i;
-        this._addDiffChunk(ctx, content[i], leftSide, rightSide);
-      }
-
-      this._diff = {
-        leftSide: leftSide,
-        rightSide: rightSide,
-      };
+        left: results.comments.baseComments.concat(baseDrafts),
+        right: results.comments.comments.concat(drafts),
+      });
     },
 
-    // In order to show comments out of the bounds of the selected context,
-    // treat them as diffs within the model so that the content (and context
-    // surrounding it) renders correctly.
-    _breakUpCommonChunksWithComments: function(ctx, content) {
-      var result = [];
-      var leftLineNum = ctx.left.lineNum;
-      var rightLineNum = ctx.right.lineNum;
-      for (var i = 0; i < content.length; i++) {
-        if (!content[i].ab) {
-          result.push(content[i]);
-          if (content[i].a) {
-            leftLineNum += content[i].a.length;
-          }
-          if (content[i].b) {
-            rightLineNum += content[i].b.length;
-          }
-          continue;
-        }
-        var chunk = content[i].ab;
-        var currentChunk = {ab: []};
-        for (var j = 0; j < chunk.length; j++) {
-          leftLineNum++;
-          rightLineNum++;
-          if (this._groupedBaseComments[leftLineNum] == null &&
-              this._groupedComments[rightLineNum] == null) {
-            currentChunk.ab.push(chunk[j]);
-          } else {
-            if (currentChunk.ab && currentChunk.ab.length > 0) {
-              result.push(currentChunk);
-              currentChunk = {ab: []};
-            }
-            // Append an annotation to indicate that this line should not be
-            // highlighted even though it's implied with both `a` and `b`
-            // defined. This is needed since there may be two lines that
-            // should be highlighted but are equal (blank lines, for example).
-            result.push({
-              __noHighlight: true,
-              a: [chunk[j]],
-              b: [chunk[j]],
-            });
-          }
-        }
-        if (currentChunk.ab != null && currentChunk.ab.length > 0) {
-          result.push(currentChunk);
-        }
-      }
-      return result;
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
     },
 
-    _groupCommentsAndDrafts: function() {
-      this._baseDrafts.forEach(function(d) { d.__draft = true; });
-      this._drafts.forEach(function(d) { d.__draft = true; });
-      var allLeft = this._baseComments.concat(this._baseDrafts);
-      var allRight = this._comments.concat(this._drafts);
-
-      var leftByLine = {};
-      var rightByLine = {};
-      var mapFunc = function(byLine) {
-        return function(c) {
-          // File comments/drafts are grouped with line 1 for now.
-          var line = c.line || 1;
-          if (byLine[line] == null) {
-            byLine[line] = [];
-          }
-          byLine[line].push(c);
-        };
-      };
-      allLeft.forEach(mapFunc(leftByLine));
-      allRight.forEach(mapFunc(rightByLine));
-
-      this._groupedBaseComments = leftByLine;
-      this._groupedComments = rightByLine;
+    _getDiffBuilder: function(diff, comments, prefs) {
+      if (this._viewMode === DiffViewMode.SIDE_BY_SIDE) {
+        return new GrDiffBuilderSideBySide(diff, comments, prefs,
+            this.$.diffTable);
+      } else if (this._viewMode === DiffViewMode.UNIFIED) {
+        return new GrDiffBuilderUnified(diff, comments, prefs,
+            this.$.diffTable);
+      }
+      throw Error('Unsupported diff view mode: ' + this._viewMode);
     },
 
-    _addContextControl: function(ctx, leftSide, rightSide) {
-      var numLinesHidden = ctx.lastNumLinesHidden;
-      var leftStart = leftSide.length - numLinesHidden;
-      var leftEnd = leftSide.length;
-      var rightStart = rightSide.length - numLinesHidden;
-      var rightEnd = rightSide.length;
-      if (leftStart != rightStart || leftEnd != rightEnd) {
-        throw Error(
-            'Left and right ranges for context control should be equal:' +
-            'Left: [' + leftStart + ', ' + leftEnd + '] ' +
-            'Right: [' + rightStart + ', ' + rightEnd + ']');
+    _projectConfigChanged: function(projectConfig) {
+      var threadEls = this._getCommentThreads();
+      for (var i = 0; i < threadEls.length; i++) {
+        threadEls[i].projectConfig = projectConfig;
       }
-      var obj = {
-        type: 'CONTEXT_CONTROL',
-        numLines: numLinesHidden,
-        start: leftStart,
-        end: leftEnd,
-      };
-      // NOTE: Be careful, here. This object is meant to be immutable. If the
-      // object is altered within one side's array it will reflect the
-      // alterations in another.
-      leftSide.push(obj);
-      rightSide.push(obj);
-    },
-
-    _addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) {
-      for (var i = 0; i < chunk.ab.length; i++) {
-        var numLines = Math.ceil(
-            this._visibleLineLength(chunk.ab[i]) / this.prefs.line_length);
-        var hidden = i >= ctx.skipRange[0] &&
-            i < chunk.ab.length - ctx.skipRange[1];
-        if (ctx.hidingLines && hidden == false) {
-          // No longer hiding lines. Add a context control.
-          this._addContextControl(ctx, leftSide, rightSide);
-          ctx.lastNumLinesHidden = 0;
-        }
-        ctx.hidingLines = hidden;
-        if (hidden) {
-          ctx.lastNumLinesHidden++;
-        }
-
-        // Blank lines within a diff content array indicate a newline.
-        leftSide.push({
-          type: 'CODE',
-          hidden: hidden,
-          content: chunk.ab[i] || '\n',
-          numLines: numLines,
-          lineNum: ++ctx.left.lineNum,
-        });
-        rightSide.push({
-          type: 'CODE',
-          hidden: hidden,
-          content: chunk.ab[i] || '\n',
-          numLines: numLines,
-          lineNum: ++ctx.right.lineNum,
-        });
-
-        this._addCommentsIfPresent(ctx, leftSide, rightSide);
-      }
-      if (ctx.lastNumLinesHidden > 0) {
-        this._addContextControl(ctx, leftSide, rightSide);
-      }
-    },
-
-    _addDiffChunk: function(ctx, chunk, leftSide, rightSide) {
-      if (chunk.ab) {
-        this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide);
-        return;
-      }
-
-      var leftHighlights = [];
-      if (chunk.edit_a) {
-        leftHighlights =
-            this._normalizeIntralineHighlights(chunk.a, chunk.edit_a);
-      }
-      var rightHighlights = [];
-      if (chunk.edit_b) {
-        rightHighlights =
-            this._normalizeIntralineHighlights(chunk.b, chunk.edit_b);
-      }
-
-      var aLen = (chunk.a && chunk.a.length) || 0;
-      var bLen = (chunk.b && chunk.b.length) || 0;
-      var maxLen = Math.max(aLen, bLen);
-      for (var i = 0; i < maxLen; i++) {
-        var hasLeftContent = chunk.a && i < chunk.a.length;
-        var hasRightContent = chunk.b && i < chunk.b.length;
-        var leftContent = hasLeftContent ? chunk.a[i] : '';
-        var rightContent = hasRightContent ? chunk.b[i] : '';
-        var highlight = !chunk.__noHighlight;
-        var maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
-        if (hasLeftContent) {
-          leftSide.push({
-            type: 'CODE',
-            content: leftContent || '\n',
-            numLines: maxNumLines,
-            lineNum: ++ctx.left.lineNum,
-            highlight: highlight,
-            intraline: highlight && leftHighlights.filter(function(hl) {
-              return hl.contentIndex == i;
-            }),
-          });
-        } else {
-          leftSide.push({
-            type: 'FILLER',
-            numLines: maxNumLines,
-          });
-        }
-        if (hasRightContent) {
-          rightSide.push({
-            type: 'CODE',
-            content: rightContent || '\n',
-            numLines: maxNumLines,
-            lineNum: ++ctx.right.lineNum,
-            highlight: highlight,
-            intraline: highlight && rightHighlights.filter(function(hl) {
-              return hl.contentIndex == i;
-            }),
-          });
-        } else {
-          rightSide.push({
-            type: 'FILLER',
-            numLines: maxNumLines,
-          });
-        }
-        this._addCommentsIfPresent(ctx, leftSide, rightSide);
-      }
-    },
-
-    _addCommentsIfPresent: function(ctx, leftSide, rightSide) {
-      var leftComments = this._groupedBaseComments[ctx.left.lineNum];
-      var rightComments = this._groupedComments[ctx.right.lineNum];
-      if (leftComments) {
-        var thread = {
-          type: 'COMMENT_THREAD',
-          comments: leftComments,
-        };
-        if (this.patchRange.basePatchNum == 'PARENT') {
-          thread.patchNum = this.patchRange.patchNum;
-        }
-        leftSide.push(thread);
-      }
-      if (rightComments) {
-        rightSide.push({
-          type: 'COMMENT_THREAD',
-          comments: rightComments,
-        });
-      }
-      if (leftComments && !rightComments) {
-        rightSide.push({type: 'FILLER'});
-      } else if (!leftComments && rightComments) {
-        leftSide.push({type: 'FILLER'});
-      }
-      this._groupedBaseComments[ctx.left.lineNum] = null;
-      this._groupedComments[ctx.right.lineNum] = null;
-    },
-
-    // The `highlights` array consists of a list of <skip length, mark length>
-    // pairs, where the skip length is the number of characters between the
-    // end of the previous edit and the start of this edit, and the mark
-    // length is the number of edited characters following the skip. The start
-    // of the edits is from the beginning of the related diff content lines.
-    //
-    // Note that the implied newline character at the end of each line is
-    // included in the length calculation, and thus it is possible for the
-    // edits to span newlines.
-    //
-    // A line highlight object consists of three fields:
-    // - contentIndex: The index of the diffChunk `content` field (the line
-    //   being referred to).
-    // - startIndex: Where the highlight should begin.
-    // - endIndex: (optional) Where the highlight should end. If omitted, the
-    //   highlight is meant to be a continuation onto the next line.
-    _normalizeIntralineHighlights: function(content, highlights) {
-      var contentIndex = 0;
-      var idx = 0;
-      var normalized = [];
-      for (var i = 0; i < highlights.length; i++) {
-        var line = content[contentIndex] + '\n';
-        var hl = highlights[i];
-        var j = 0;
-        while (j < hl[0]) {
-          if (idx == line.length) {
-            idx = 0;
-            line = content[++contentIndex] + '\n';
-            continue;
-          }
-          idx++;
-          j++;
-        }
-        var lineHighlight = {
-          contentIndex: contentIndex,
-          startIndex: idx,
-        };
-
-        j = 0;
-        while (line && j < hl[1]) {
-          if (idx == line.length) {
-            idx = 0;
-            line = content[++contentIndex] + '\n';
-            normalized.push(lineHighlight);
-            lineHighlight = {
-              contentIndex: contentIndex,
-              startIndex: idx,
-            };
-            continue;
-          }
-          idx++;
-          j++;
-        }
-        lineHighlight.endIndex = idx;
-        normalized.push(lineHighlight);
-      }
-      return normalized;
-    },
-
-    _visibleLineLength: function(contents) {
-      // http://jsperf.com/performance-of-match-vs-split
-      var numTabs = contents.split('\t').length - 1;
-      return contents.length - numTabs + (this.prefs.tab_size * numTabs);
-    },
-
-    _maxLinesSpanned: function(left, right) {
-      return Math.max(
-          Math.ceil(this._visibleLineLength(left) / this.prefs.line_length),
-          Math.ceil(this._visibleLineLength(right) / this.prefs.line_length));
     },
   });
 })();
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 91f5f3b..579957e 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,8 +20,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.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">
 <link rel="import" href="gr-diff.html">
@@ -35,458 +33,214 @@
 <script>
   suite('gr-diff tests', function() {
     var element;
-    var server;
-    var getDiffStub;
-    var getCommentsStub;
 
     setup(function() {
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(false); },
+      })
       element = fixture('basic');
-      element.changeNum = 42;
-      element.path = 'sieve.go';
-      element.prefs = {
-        context: 10,
-        tab_size: 8,
-      };
-
-      getDiffStub = sinon.stub(element.$.restAPI, 'getDiff', function() {
-        return Promise.resolve({
-          change_type: 'MODIFIED',
-          content: [
-            {
-              ab: [
-                '<!DOCTYPE html>',
-                '<meta charset="utf-8">',
-                '<title>My great page</title>',
-                '<style>',
-                '  *,',
-                '  *:before,',
-                '  *:after {',
-                '    box-sizing: border-box;',
-                '  }',
-                '</style>',
-                '<header>',
-              ]
-            },
-            {
-              a: [
-                '  Welcome ',
-                '  to the wooorld of tomorrow!',
-              ],
-              b: [
-                '  Hello, world!',
-              ],
-            },
-            {
-              ab: [
-                '</header>',
-                '<body>',
-                'Leela: This is the only place the ship can’t hear us, so ',
-                'everyone pretend to shower.',
-                'Fry: Same as every day. Got it.',
-              ]
-            },
-          ]
-        });
-      });
-
-      getCommentsStub = sinon.stub(element.$.restAPI, 'getDiffComments',
-        function() {
-          return Promise.resolve({
-            baseComments: [
-              {
-                author: {
-                  _account_id: 1000000,
-                  name: 'Andrew Bonventre',
-                  email: 'andybons@gmail.com',
-                },
-                id: '9af53d3f_5f2b8b82',
-                line: 1,
-                message: 'this isn’t quite right',
-                updated: '2015-12-10 02:50:21.627000000',
-              }
-            ],
-            comments: [
-              {
-                author: {
-                  _account_id: 1010008,
-                  name: 'Dave Borowitz',
-                  email: 'dborowitz@google.com',
-                },
-                id: '001a2067_f30f3048',
-                line: 12,
-                message: 'What on earth are you thinking, here?',
-                updated: '2015-12-12 02:51:37.973000000',
-              },
-              {
-                author: {
-                  _account_id: 1000000,
-                  name: 'Andrew Bonventre',
-                  email: 'andybons@gmail.com',
-                },
-                id: 'a0407443_30dfe8fb',
-                in_reply_to: '001a2067_f30f3048',
-                line: 12,
-                message: '¯\\_(ツ)_/¯',
-                updated: '2015-12-12 18:50:21.627000000',
-              },
-            ],
-          });
-        }
-      );
-
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'PUT',
-        '/accounts/self/preferences.diff',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify({context: 25}),
-        ]
-      );
-
     });
 
-    teardown(function() {
-      getDiffStub.restore();
-      getCommentsStub.restore();
-      server.restore();
-    });
+    test('get drafts logged out', function(done) {
+      element.patchRange = {basePatchNum: 0, patchNum: 0};
 
-    test('comment rendering', function(done) {
-      element.prefs.context = -1;
-      element._loggedIn = true;
-      element.patchRange = {
-        basePatchNum: 1,
-        patchNum: 2,
-      };
-
-      element.reload().then(function() {
-        flush(function() {
-          var leftThreadEls =
-              Polymer.dom(element.$.leftDiff.root).querySelectorAll(
-                  'gr-diff-comment-thread');
-          assert.equal(leftThreadEls.length, 1);
-          assert.equal(leftThreadEls[0].comments.length, 1);
-
-          var rightThreadEls =
-              Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                  'gr-diff-comment-thread');
-          assert.equal(rightThreadEls.length, 1);
-          assert.equal(rightThreadEls[0].comments.length, 2);
-
-          var index = leftThreadEls[0].getAttribute('data-index');
-          var leftFillerEls =
-              Polymer.dom(element.$.leftDiff.root).querySelectorAll(
-                  '.commentThread.filler[data-index="' + index + '"]');
-          assert.equal(leftFillerEls.length, 1);
-          var rightFillerEls =
-              Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                  '[data-index="' + index + '"]');
-          assert.equal(rightFillerEls.length, 2);
-
-          for (var i = 0; i < rightFillerEls.length; i++) {
-            assert.isTrue(rightFillerEls[i].classList.contains('filler'));
-          }
-          var originalHeight = rightFillerEls[0].offsetHeight;
-          assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
-          assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
-          assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
-
-          // Create a comment on the opposite side of the first comment.
-          var rightLineEL = element.$.rightDiff.$$(
-                '.lineNum[data-index="' + (index - 1) + '"]');
-          assert.ok(rightLineEL);
-          MockInteractions.tap(rightLineEL);
-          flush(function() {
-            var newThreadEls =
-              Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                  '[data-index="' + index + '"]');
-            assert.equal(newThreadEls.length, 2);
-            for (var i = 0; i < newThreadEls.length; i++) {
-              assert.isTrue(
-                  newThreadEls[i].classList.contains('commentThread') ||
-                  newThreadEls[i].tagName == 'GR-DIFF-COMMENT-THREAD');
-            }
-            var newHeight = newThreadEls[0].offsetHeight;
-            assert.equal(newThreadEls[1].offsetHeight, newHeight);
-            assert.equal(leftFillerEls[0].offsetHeight, newHeight);
-            assert.equal(leftThreadEls[0].offsetHeight, newHeight);
-
-            // The editing mode height of the right comment will be greater than
-            // the non-editing mode height of the left comment.
-            assert.isAbove(newHeight, originalHeight);
-
-            // Discard the right thread and ensure the left comment heights are
-            // back to their original values.
-            newThreadEls[1].addEventListener('discard', function() {
-              rightFillerEls =
-                  Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                      '[data-index="' + index + '"]');
-              assert.equal(rightFillerEls.length, 2);
-
-              for (var i = 0; i < rightFillerEls.length; i++) {
-                assert.isTrue(rightFillerEls[i].classList.contains('filler'));
-              }
-              var originalHeight = rightFillerEls[0].offsetHeight;
-              assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
-              assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
-              assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
-              done();
-            });
-            var commentEl = newThreadEls[1].$$('gr-diff-comment');
-            commentEl.fire('discard', null, {bubbles: false});
-          });
-        });
-      });
-      server.respond();
-    });
-
-    test('intraline normalization', function() {
-      // The content and highlights are in the format returned by the Gerrit
-      // REST API.
-      var content = [
-        '      <section class="summary">',
-        '        <gr-linked-text content="' +
-            '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
-        '      </section>',
-      ];
-      var highlights = [
-        [31, 34], [42, 26]
-      ];
-      var results = element._normalizeIntralineHighlights(content, highlights);
-      assert.deepEqual(results, [
-        {
-          contentIndex: 0,
-          startIndex: 31,
-        },
-        {
-          contentIndex: 1,
-          startIndex: 0,
-          endIndex: 33,
-        },
-        {
-          contentIndex: 1,
-          startIndex: 75,
-        },
-        {
-          contentIndex: 2,
-          startIndex: 0,
-          endIndex: 6,
-        }
-      ]);
-
-      content = [
-        '        this._path = value.path;',
-        '',
-        '        // When navigating away from the page, there is a possibility that the',
-        '        // patch number is no longer a part of the URL (say when navigating to',
-        '        // the top-level change info view) and therefore undefined in `params`.',
-        '        if (!this._patchRange.patchNum) {',
-      ];
-      highlights = [
-        [14, 17],
-        [11, 70],
-        [12, 67],
-        [12, 67],
-        [14, 29],
-      ];
-      results = element._normalizeIntralineHighlights(content, highlights);
-      assert.deepEqual(results, [
-        {
-          contentIndex: 0,
-          startIndex: 14,
-          endIndex: 31,
-        },
-        {
-          contentIndex: 2,
-          startIndex: 8,
-          endIndex: 78,
-        },
-        {
-          contentIndex: 3,
-          startIndex: 11,
-          endIndex: 78,
-        },
-        {
-          contentIndex: 4,
-          startIndex: 11,
-          endIndex: 78,
-        },
-        {
-          contentIndex: 5,
-          startIndex: 12,
-          endIndex: 41,
-        }
-      ]);
-    });
-
-    test('context', function() {
-      element.prefs.context = 3;
-      element._diffResponse = {
-        content: [
-          {
-            ab: [
-              '<!DOCTYPE html>',
-              '<meta charset="utf-8">',
-              '<title>My great page</title>',
-              '<style>',
-              '  *,',
-              '  *:before,',
-              '  *:after {',
-              '    box-sizing: border-box;',
-              '  }',
-              '</style>',
-              '<header>',
-            ]
-          },
-          {
-            a: [
-              '  Welcome ',
-              '  to the wooorld of tomorrow!',
-            ],
-            b: [
-              '  Hello, world!',
-            ],
-          },
-          {
-            ab: [
-              '</header>',
-              '<body>',
-              'Leela: This is the only place the ship can’t hear us, so ',
-              'everyone pretend to shower.',
-              'Fry: Same as every day. Got it.',
-            ]
-          },
-        ]
-      };
-      element._processContent();
-
-      // First eight lines should be hidden on both sides.
-      for (var i = 0; i < 8; i++) {
-        assert.isTrue(element._diff.leftSide[i].hidden);
-        assert.isTrue(element._diff.rightSide[i].hidden);
-      }
-      // A context control should be at index 8 on both sides.
-      var leftContext = element._diff.leftSide[8];
-      var rightContext = element._diff.rightSide[8];
-      assert.deepEqual(leftContext, rightContext);
-      assert.equal(leftContext.numLines, 8);
-      assert.equal(leftContext.start, 0);
-      assert.equal(leftContext.end, 8);
-
-      // Line indices 9-16 should be shown.
-      for (var i = 9; i <= 16; i++) {
-        // notOk (falsy) because the `hidden` attribute may not be present.
-        assert.notOk(element._diff.leftSide[i].hidden);
-        assert.notOk(element._diff.rightSide[i].hidden);
-      }
-
-      // Lines at indices 17 and 18 should be hidden.
-      assert.isTrue(element._diff.leftSide[17].hidden);
-      assert.isTrue(element._diff.rightSide[17].hidden);
-      assert.isTrue(element._diff.leftSide[18].hidden);
-      assert.isTrue(element._diff.rightSide[18].hidden);
-
-      // Context control at index 19.
-      leftContext = element._diff.leftSide[19];
-      rightContext = element._diff.rightSide[19];
-      assert.deepEqual(leftContext, rightContext);
-      assert.equal(leftContext.numLines, 2);
-      assert.equal(leftContext.start, 17);
-      assert.equal(leftContext.end, 19);
-    });
-
-    test('save prefs', function(done) {
-      element._loggedIn = false;
-
-      element.prefs = {
-        tab_size: 4,
-        context: 50,
-      };
-      element.fire('save', {}, {node: element.$$('gr-diff-preferences')});
-      assert.isTrue(element._diffPreferencesPromise == null);
-
-      element._loggedIn = true;
-      element.fire('save', {}, {node: element.$$('gr-diff-preferences')});
-      server.respond();
-
-      element._diffPreferencesPromise.then(function(req) {
-        assert.equal(req.xhr.requestBody, JSON.stringify(element.prefs));
+      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts');
+      var loggedInStub = sinon.stub(element, '_getLoggedIn',
+          function() { return Promise.resolve(false); });
+      element._getDiffDrafts().then(function(result) {
+        assert.deepEqual(result, {baseComments: [], comments: []});
+        sinon.assert.notCalled(getDraftsStub);
+        loggedInStub.restore();
+        getDraftsStub.restore();
         done();
       });
     });
 
-    test('visible line length', function() {
-      assert.equal(element._visibleLineLength('A'.repeat(5)), 5);
-      assert.equal(
-          element._visibleLineLength('A'.repeat(5) + '\t' + 'A'.repeat(5)), 18);
+    test('get drafts logged in', function(done) {
+      element.patchRange = {basePatchNum: 0, patchNum: 0};
+      var draftsResponse = {
+        baseComments: [{id: 'foo'}],
+        comments: [{id: 'bar'}],
+      };
+      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts',
+          function() { return Promise.resolve(draftsResponse); });
+      var loggedInStub = sinon.stub(element, '_getLoggedIn',
+          function() { return Promise.resolve(true); });
+      element._getDiffDrafts().then(function(result) {
+        assert.deepEqual(result, draftsResponse);
+        loggedInStub.restore();
+        getDraftsStub.restore();
+        done();
+      });
     });
 
-    test('break up common diff chunks', function() {
-      element._groupedBaseComments = {
-        1: {},
+    test('get comments and drafts', function(done) {
+      var loggedInStub = sinon.stub(element, '_getLoggedIn',
+          function() { return Promise.resolve(true); });
+      var comments = {
+        baseComments: [
+          {id: 'bc1'},
+          {id: 'bc2'},
+        ],
+        comments: [
+          {id: 'c1'},
+          {id: 'c2'},
+        ],
       };
-      element._groupedComments = {
-        10: {},
+      var diffCommentsStub = sinon.stub(element, '_getDiffComments',
+          function() { return Promise.resolve(comments); });
+
+      var drafts = {
+        baseComments: [
+          {id: 'bd1'},
+          {id: 'bd2'},
+        ],
+        comments: [
+          {id: 'd1'},
+          {id: 'd2'},
+        ],
       };
-      var ctx = {
-        left: {lineNum: 0},
-        right: {lineNum: 0},
+      var diffDraftsStub = sinon.stub(element, '_getDiffDrafts',
+          function() { return Promise.resolve(drafts); });
+
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 3,
       };
-      var content = [
-        {
-          ab: [
-            'Copyright (C) 2015 The Android Open Source Project',
-            '',
-            'Licensed under the Apache License, Version 2.0 (the "License");',
-            'you may not use this file except in compliance with the License.',
-            'You may obtain a copy of the License at',
-            '',
-            'http://www.apache.org/licenses/LICENSE-2.0',
-            '',
-            'Unless required by applicable law or agreed to in writing, ',
-            'software distributed under the License is distributed on an ',
-            '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ',
-            'either express or implied. See the License for the specific ',
-            'language governing permissions and limitations under the License.',
-          ]
-        }
-      ];
-      var result = element._breakUpCommonChunksWithComments(ctx, content);
-      assert.deepEqual(result, [
-        {
-          __noHighlight: true,
-          a: ['Copyright (C) 2015 The Android Open Source Project'],
-          b: ['Copyright (C) 2015 The Android Open Source Project'],
+      element.path = '/path/to/foo';
+      element.projectConfig = {foo: 'bar'};
+
+      element._getDiffCommentsAndDrafts().then(function(result) {
+        assert.deepEqual(result, {
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1'},
+            {id: 'bc2'},
+            {id: 'bd1', __draft: true},
+            {id: 'bd2', __draft: true},
+          ],
+          right: [
+            {id: 'c1'},
+            {id: 'c2'},
+            {id: 'd1', __draft: true},
+            {id: 'd2', __draft: true},
+          ],
+        });
+
+        diffCommentsStub.restore();
+        diffDraftsStub.restore();
+        loggedInStub.restore();
+        done();
+      });
+    });
+
+    test('remove comment', function() {
+      element._comments = {
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
         },
-        {
-          ab: [
-            '',
-            '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, ',
-          ]
+        left: [
+          {id: 'bc1'},
+          {id: 'bc2'},
+          {id: 'bd1', __draft: true},
+          {id: 'bd2', __draft: true},
+        ],
+        right: [
+          {id: 'c1'},
+          {id: 'c2'},
+          {id: 'd1', __draft: true},
+          {id: 'd2', __draft: true},
+        ],
+      };
+
+      element._removeComment({});
+      // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem to
+      // believe that one object deepEquals another even when they do :-/.
+      assert.equal(JSON.stringify(element._comments), JSON.stringify({
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
         },
-        {
-          __noHighlight: true,
-          a: ['software distributed under the License is distributed on an '],
-          b: ['software distributed under the License is distributed on an ']
+        left: [
+          {id: 'bc1'},
+          {id: 'bc2'},
+          {id: 'bd1', __draft: true},
+          {id: 'bd2', __draft: true},
+        ],
+        right: [
+          {id: 'c1'},
+          {id: 'c2'},
+          {id: 'd1', __draft: true},
+          {id: 'd2', __draft: true},
+        ],
+      }));
+
+      element._removeComment({id: 'bc2'});
+      assert.equal(JSON.stringify(element._comments), JSON.stringify({
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
         },
-        {
-          ab: [
-            '"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.',
-          ]
-        }
-      ]);
+        left: [
+          {id: 'bc1'},
+          {id: 'bd1', __draft: true},
+          {id: 'bd2', __draft: true},
+        ],
+        right: [
+          {id: 'c1'},
+          {id: 'c2'},
+          {id: 'd1', __draft: true},
+          {id: 'd2', __draft: true},
+        ],
+      }));
+
+      element._removeComment({id: 'd2'});
+      assert.deepEqual(JSON.stringify(element._comments), JSON.stringify({
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1'},
+          {id: 'bd1', __draft: true},
+          {id: 'bd2', __draft: true},
+        ],
+        right: [
+          {id: 'c1'},
+          {id: 'c2'},
+          {id: 'd1', __draft: true},
+        ],
+      }));
     });
   });
-
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder.js
deleted file mode 100644
index 0e14cac..0000000
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder.js
+++ /dev/null
@@ -1,301 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the 'License');
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function(window, GrDiffGroup, GrDiffLine) {
-  'use strict';
-
-  function GrDiffBuilder(diff, prefs, outputEl) {
-    this._prefs = prefs;
-    this._outputEl = outputEl;
-    this._groups = [];
-
-    this._processContent(diff.content, this._groups, prefs.context);
-  }
-
-  GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0);
-  GrDiffBuilder.GREATER_THAN_CODE = '>'.charCodeAt(0);
-  GrDiffBuilder.AMPERSAND_CODE = '&'.charCodeAt(0);
-  GrDiffBuilder.SEMICOLON_CODE = ';'.charCodeAt(0);
-
-  GrDiffBuilder.TAB_REGEX = /\t/g;
-
-  GrDiffBuilder.LINE_FEED_HTML =
-      '<span class="style-scope gr-new-diff br"></span>';
-
-  GrDiffBuilder.GroupType = {
-    ADDED: 'b',
-    BOTH: 'ab',
-    REMOVED: 'a',
-  };
-
-  GrDiffBuilder.prototype.emitDiff = function() {
-    for (var i = 0; i < this._groups.length; i++) {
-      this.emitGroup(this._groups[i]);
-    }
-  };
-
-  GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
-    throw Error('Subclasses must implement emitGroup');
-  },
-
-  GrDiffBuilder.prototype._processContent = function(content, groups, context) {
-    var WHOLE_FILE = -1;
-    context = content.length > 1 ? context : WHOLE_FILE;
-
-    var lineNums = {
-      left: 0,
-      right: 0,
-    };
-
-    for (var i = 0; i < content.length; i++) {
-      var group = content[i];
-      var lines = [];
-
-      if (group[GrDiffBuilder.GroupType.BOTH] !== undefined) {
-        var rows = group[GrDiffBuilder.GroupType.BOTH];
-        this._appendCommonLines(rows, lines, lineNums);
-
-        var hiddenRange = [context, rows.length - context];
-        if (i === 0) {
-          hiddenRange[0] = 0;
-        } else if (i === content.length - 1) {
-          hiddenRange[1] = rows.length;
-        }
-
-        if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 0) {
-          this._insertContextGroups(groups, lines, hiddenRange);
-        } else {
-          groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, lines));
-        }
-        continue;
-      }
-
-      if (group[GrDiffBuilder.GroupType.REMOVED] !== undefined) {
-        this._appendRemovedLines(group[GrDiffBuilder.GroupType.REMOVED], lines,
-            lineNums);
-      }
-      if (group[GrDiffBuilder.GroupType.ADDED] !== undefined) {
-        this._appendAddedLines(group[GrDiffBuilder.GroupType.ADDED], lines,
-            lineNums);
-      }
-      groups.push(new GrDiffGroup(GrDiffGroup.Type.DELTA, lines));
-    }
-  };
-
-  GrDiffBuilder.prototype._insertContextGroups = function(groups, lines,
-      hiddenRange) {
-    // TODO: Split around comments as well.
-    var linesBeforeCtx = lines.slice(0, hiddenRange[0]);
-    var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
-    var linesAfterCtx = lines.slice(hiddenRange[1]);
-
-    if (linesBeforeCtx.length > 0) {
-      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
-    }
-
-    var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
-    ctxLine.contextLines = hiddenLines;
-    groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
-        [ctxLine]));
-
-    if (linesAfterCtx.length > 0) {
-      groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
-    }
-  };
-
-  GrDiffBuilder.prototype._appendCommonLines = function(rows, lines, lineNums) {
-    for (var i = 0; i < rows.length; i++) {
-      var line = new GrDiffLine(GrDiffLine.Type.BOTH);
-      line.text = rows[i];
-      line.beforeNumber = ++lineNums.left;
-      line.afterNumber = ++lineNums.right;
-      lines.push(line);
-    }
-  };
-
-  GrDiffBuilder.prototype._appendRemovedLines = function(rows, lines,
-      lineNums) {
-    for (var i = 0; i < rows.length; i++) {
-      var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
-      line.text = rows[i];
-      line.beforeNumber = ++lineNums.left;
-      lines.push(line);
-    }
-  };
-
-  GrDiffBuilder.prototype._appendAddedLines = function(rows, lines, lineNums) {
-    for (var i = 0; i < rows.length; i++) {
-      var line = new GrDiffLine(GrDiffLine.Type.ADD);
-      line.text = rows[i];
-      line.afterNumber = ++lineNums.right;
-      lines.push(line);
-    }
-  };
-
-  GrDiffBuilder.prototype._createContextControl = function(section, line) {
-    if (!line.contextLines.length) {
-      return null;
-    }
-    var td = this._createElement('td');
-    var button = this._createElement('gr-button', 'showContext');
-    button.setAttribute('link', true);
-    var commonLines = line.contextLines.length;
-    var text = 'Show ' + commonLines + ' common line';
-    if (commonLines > 1) {
-      text += 's';
-    }
-    text += '...';
-    button.textContent = text;
-    button.addEventListener('tap', function(e) {
-      e.detail = {
-        group: new GrDiffGroup(GrDiffGroup.Type.BOTH, line.contextLines),
-        section: section,
-      };
-      // Let it bubble up the DOM tree.
-    });
-    td.appendChild(button);
-    return td;
-  };
-
-  GrDiffBuilder.prototype._createLineEl = function(line, number, type) {
-    var td = this._createElement('td', 'lineNum');
-    if (line.type === GrDiffLine.Type.BLANK) {
-      return td;
-    } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
-      td.setAttribute('data-value', '@@');
-    } else if (line.type === GrDiffLine.Type.BOTH || line.type == type) {
-      td.setAttribute('data-value', number);
-    }
-    return td;
-  };
-
-  GrDiffBuilder.prototype._createTextEl = function(line) {
-    var td = this._createElement('td');
-    if (line.type !== GrDiffLine.Type.BLANK) {
-      td.classList.add('content');
-    }
-    td.classList.add(line.type);
-    var text = line.text;
-    var html = util.escapeHTML(text);
-    if (text.length > this._prefs.line_length) {
-      html = this._addNewlines(text, html);
-    }
-    html = this._addTabWrappers(html);
-
-    // If the html is equivalent to the text then it didn't get highlighted
-    // or escaped. Use textContent which is faster than innerHTML.
-    if (html == text) {
-      td.textContent = text;
-    } else {
-      td.innerHTML = html;
-    }
-    return td;
-  };
-
-  // Advance `index` by the appropriate number of characters that would
-  // represent one source code character and return that index. For
-  // example, for source code '<span>' the escaped html string is
-  // '&lt;span&gt;'. Advancing from index 0 on the prior html string would
-  // return 4, since &lt; maps to one source code character ('<').
-  GrDiffBuilder.prototype._advanceChar = function(html, index) {
-    // TODO(andybons): Unicode is all kinds of messed up in JS. Account for it.
-    // https://mathiasbynens.be/notes/javascript-unicode
-
-    // Tags don't count as characters
-    while (index < html.length &&
-           html.charCodeAt(index) == GrDiffBuilder.LESS_THAN_CODE) {
-      while (index < html.length &&
-             html.charCodeAt(index) != GrDiffBuilder.GREATER_THAN_CODE) {
-        index++;
-      }
-      index++;  // skip the ">" itself
-    }
-    // An HTML entity (e.g., &lt;) counts as one character.
-    if (index < html.length &&
-        html.charCodeAt(index) == GrDiffBuilder.AMPERSAND_CODE) {
-      while (index < html.length &&
-             html.charCodeAt(index) != GrDiffBuilder.SEMICOLON_CODE) {
-        index++;
-      }
-    }
-    return index + 1;
-  };
-
-  GrDiffBuilder.prototype._addNewlines = function(text, html) {
-    var htmlIndex = 0;
-    var indices = [];
-    var numChars = 0;
-    for (var i = 0; i < text.length; i++) {
-      if (numChars > 0 && numChars % this._prefs.line_length === 0) {
-        indices.push(htmlIndex);
-      }
-      htmlIndex = this._advanceChar(html, htmlIndex);
-      if (text[i] === '\t') {
-        numChars += this._prefs.tab_size;
-      } else {
-        numChars++;
-      }
-    }
-    var result = html;
-    // Since the result string is being altered in place, start from the end
-    // of the string so that the insertion indices are not affected as the
-    // result string changes.
-    for (var i = indices.length - 1; i >= 0; i--) {
-      result = result.slice(0, indices[i]) + GrDiffBuilder.LINE_FEED_HTML +
-          result.slice(indices[i]);
-    }
-    return result;
-  };
-
-  GrDiffBuilder.prototype._addTabWrappers = function(html) {
-    var htmlStr = this._getTabWrapper(this._prefs.tab_size,
-        this._prefs.show_tabs);
-    return html.replace(GrDiffBuilder.TAB_REGEX, htmlStr);
-  };
-
-  GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) {
-    // Force this to be a number to prevent arbitrary injection.
-    tabSize = +tabSize;
-    if (isNaN(tabSize)) {
-      throw Error('Invalid tab size from preferences.');
-    }
-
-    var str = '<span class="style-scope gr-new-diff tab ';
-    if (showTabs) {
-      str += 'withIndicator';
-    }
-    str += '" ';
-    // TODO(andybons): CSS tab-size is not supported in IE.
-    str += 'style="tab-size:' + tabSize + ';';
-    str += 'style="-moz-tab-size:' + tabSize + ';';
-    str += '">\t</span>';
-    return str;
-  };
-
-  GrDiffBuilder.prototype._createElement = function(tagName, className) {
-    var el = document.createElement(tagName);
-    // When Shady DOM is being used, these classes are added to account for
-    // Polymer's polyfill behavior. In order to guarantee sufficient
-    // specificity within the CSS rules, these are added to every element.
-    // Since the Polymer DOM utility functions (which would do this
-    // automatically) are not being used for performance reasons, this is
-    // done manually.
-    el.classList.add('style-scope', 'gr-new-diff');
-    if (!!className) {
-      el.classList.add(className);
-    }
-    return el;
-  };
-
-  window.GrDiffBuilder = GrDiffBuilder;
-})(window, GrDiffGroup, GrDiffLine);
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder_test.html
deleted file mode 100644
index 17f88c6..0000000
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder_test.html
+++ /dev/null
@@ -1,256 +0,0 @@
-<!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-diff-builder</title>
-
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="gr-diff-line.js"></script>
-<script src="gr-diff-group.js"></script>
-<script src="gr-diff-builder.js"></script>
-
-<script>
-  suite('gr-diff-builder tests', function() {
-
-    test('process loaded content', function() {
-      var content = [
-        {
-          ab: [
-            '<!DOCTYPE html>',
-            '<meta charset="utf-8">',
-          ]
-        },
-        {
-          a: [
-            '  Welcome ',
-            '  to the wooorld of tomorrow!',
-          ],
-          b: [
-            '  Hello, world!',
-          ],
-        },
-        {
-          ab: [
-            'Leela: This is the only place the ship can’t hear us, so ',
-            'everyone pretend to shower.',
-            'Fry: Same as every day. Got it.',
-          ]
-        },
-      ];
-      var groups = [];
-      GrDiffBuilder.prototype._processContent(content, groups, -1);
-
-      assert.equal(groups.length, 3);
-
-      var group = groups[0];
-      assert.equal(group.type, GrDiffGroup.Type.BOTH);
-      assert.equal(group.lines.length, 2);
-      assert.equal(group.lines.length, 2);
-
-      function beforeNumberFn(l) { return l.beforeNumber; }
-      function afterNumberFn(l) { return l.afterNumber; }
-      function textFn(l) { return l.text; }
-
-      assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
-      assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
-      assert.deepEqual(group.lines.map(textFn), [
-        '<!DOCTYPE html>',
-        '<meta charset="utf-8">',
-      ]);
-
-      group = groups[1];
-      assert.equal(group.type, GrDiffGroup.Type.DELTA);
-      assert.equal(group.lines.length, 3);
-      assert.equal(group.adds.length, 1);
-      assert.equal(group.removes.length, 2);
-      assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
-      assert.deepEqual(group.adds.map(afterNumberFn), [3]);
-      assert.deepEqual(group.removes.map(textFn), [
-        '  Welcome ',
-        '  to the wooorld of tomorrow!',
-      ]);
-      assert.deepEqual(group.adds.map(textFn), [
-        '  Hello, world!',
-      ]);
-
-      group = groups[2];
-      assert.equal(group.type, GrDiffGroup.Type.BOTH);
-      assert.equal(group.lines.length, 3);
-      assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
-      assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
-      assert.deepEqual(group.lines.map(textFn), [
-        'Leela: This is the only place the ship can’t hear us, so ',
-        'everyone pretend to shower.',
-        'Fry: Same as every day. Got it.',
-      ]);
-    });
-
-    test('insert context groups', function() {
-      var content = [
-        {ab: []},
-        {a: ['all work and no play make andybons a dull boy']},
-        {ab: []},
-        {b: ['elgoog elgoog elgoog']},
-        {ab: []},
-      ];
-      for (var i = 0; i < 100; i++) {
-        content[0].ab.push('all work and no play make jack a dull boy');
-        content[4].ab.push('all work and no play make jill a dull girl');
-      }
-      for (var i = 0; i < 5; i++) {
-        content[2].ab.push('no tv and no beer make homer go crazy');
-      }
-      var groups = [];
-      var context = 10;
-
-      GrDiffBuilder.prototype._processContent(content, groups, context);
-
-      assert.equal(groups[0].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.equal(groups[0].lines[0].contextLines.length, 90);
-      groups[0].lines[0].contextLines.forEach(function(l) {
-        assert.equal(l.text, content[0].ab[0]);
-      });
-
-      assert.equal(groups[1].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[1].lines.length, context);
-      groups[1].lines.forEach(function(l) {
-        assert.equal(l.text, content[0].ab[0]);
-      });
-
-      assert.equal(groups[2].type, GrDiffGroup.Type.DELTA);
-      assert.equal(groups[2].lines.length, 1);
-      assert.equal(groups[2].removes.length, 1);
-      assert.equal(groups[2].removes[0].text,
-          'all work and no play make andybons a dull boy');
-
-      assert.equal(groups[3].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[3].lines.length, 5);
-      groups[3].lines.forEach(function(l) {
-        assert.equal(l.text, content[2].ab[0]);
-      });
-
-      assert.equal(groups[4].type, GrDiffGroup.Type.DELTA);
-      assert.equal(groups[4].lines.length, 1);
-      assert.equal(groups[4].adds.length, 1);
-      assert.equal(groups[4].adds[0].text, 'elgoog elgoog elgoog');
-
-      assert.equal(groups[5].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[5].lines.length, context);
-      groups[5].lines.forEach(function(l) {
-        assert.equal(l.text, content[4].ab[0]);
-      });
-
-      assert.equal(groups[6].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.equal(groups[6].lines[0].contextLines.length, 90);
-      groups[6].lines[0].contextLines.forEach(function(l) {
-        assert.equal(l.text, content[4].ab[0]);
-      });
-
-      content = [
-        {a: ['all work and no play make andybons a dull boy']},
-        {ab: []},
-        {b: ['elgoog elgoog elgoog']},
-      ];
-      for (var i = 0; i < 50; i++) {
-        content[1].ab.push('no tv and no beer make homer go crazy');
-      }
-      groups = [];
-
-      GrDiffBuilder.prototype._processContent(content, groups, 10);
-
-      assert.equal(groups[0].type, GrDiffGroup.Type.DELTA);
-      assert.equal(groups[0].lines.length, 1);
-      assert.equal(groups[0].removes.length, 1);
-      assert.equal(groups[0].removes[0].text,
-          'all work and no play make andybons a dull boy');
-
-      assert.equal(groups[1].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[1].lines.length, context);
-      groups[1].lines.forEach(function(l) {
-        assert.equal(l.text, content[1].ab[0]);
-      });
-
-      assert.equal(groups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL);
-      assert.equal(groups[2].lines[0].contextLines.length, 30);
-      groups[2].lines[0].contextLines.forEach(function(l) {
-        assert.equal(l.text, content[1].ab[0]);
-      });
-
-      assert.equal(groups[3].type, GrDiffGroup.Type.BOTH);
-      assert.equal(groups[3].lines.length, context);
-      groups[3].lines.forEach(function(l) {
-        assert.equal(l.text, content[1].ab[0]);
-      });
-
-      assert.equal(groups[4].type, GrDiffGroup.Type.DELTA);
-      assert.equal(groups[4].lines.length, 1);
-      assert.equal(groups[4].adds.length, 1);
-      assert.equal(groups[4].adds[0].text, 'elgoog elgoog elgoog');
-    });
-
-    test('newlines', function() {
-      var prefs = {
-        line_length: 10,
-        tab_size: 4,
-      };
-      var builder = new GrDiffBuilder({content: []}, prefs);
-
-      var text = 'abcdef';
-      assert.equal(builder._addNewlines(text, text), text);
-      text = 'a'.repeat(20);
-      assert.equal(builder._addNewlines(text, text),
-          'a'.repeat(10) +
-          GrDiffBuilder.LINE_FEED_HTML +
-          'a'.repeat(10));
-
-      text = '<span class="thumbsup">👍</span>';
-      var html = '&lt;span class=&quot;thumbsup&quot;&gt;👍&lt;&#x2F;span&gt;';
-      assert.equal(builder._addNewlines(text, html),
-          '&lt;span clas' +
-          GrDiffBuilder.LINE_FEED_HTML +
-          's=&quot;thumbsu' +
-          GrDiffBuilder.LINE_FEED_HTML +
-          'p&quot;&gt;👍&lt;&#x2F;spa' +
-          GrDiffBuilder.LINE_FEED_HTML +
-          'n&gt;');
-
-      text = '01234\t56789';
-      assert.equal(builder._addNewlines(text, text),
-          '01234\t5' +
-          GrDiffBuilder.LINE_FEED_HTML +
-          '6789');
-    });
-
-    test('tab wrapper insertion', function() {
-      var prefs = {
-        show_tabs: true,
-        tab_size: 4,
-      };
-      var builder = new GrDiffBuilder({content: []}, prefs);
-
-      var html = 'abc\tdef';
-      var wrapper = builder._getTabWrapper(prefs.tab_size, prefs.show_tabs);
-      assert.ok(wrapper);
-      assert.isAbove(wrapper.length, 0);
-      assert.equal(builder._addTabWrappers(html), 'abc' + wrapper + 'def');
-      assert.throws(builder._getTabWrapper.bind(
-          builder,
-          '"><img src="/" onerror="alert(1);"><span class="',
-          true));
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.html b/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.html
deleted file mode 100644
index d266f86..0000000
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.html
+++ /dev/null
@@ -1,156 +0,0 @@
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
-<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
-
-<dom-module id="gr-new-diff">
-  <template>
-    <style>
-      :host {
-        --light-remove-highlight-color: #fee;
-        --dark-remove-highlight-color: #ffd4d4;
-        --light-add-highlight-color: #efe;
-        --dark-add-highlight-color: #d4ffd4;
-      }
-      .loading {
-        padding: 0 var(--default-horizontal-margin) 1em;
-        color: #666;
-      }
-      .header {
-        display: flex;
-        justify-content: space-between;
-        margin: 0 var(--default-horizontal-margin) .75em;
-      }
-      .prefsButton {
-        text-align: right;
-      }
-      .diffContainer {
-        border-bottom: 1px solid #eee;
-        border-top: 1px solid #eee;
-        display: flex;
-        font: 12px var(--monospace-font-family);
-        overflow-x: auto;
-        will-change: transform;
-      }
-      table {
-        border-collapse: collapse;
-        border-right: 1px solid #ddd;
-      }
-      .lineNum,
-      .content {
-        vertical-align: top;
-        white-space: pre;
-      }
-      .lineNum {
-        background-color: #eee;
-        color: #666;
-        padding: 0 .75em;
-        text-align: right;
-      }
-      .lineNum:before {
-        content: attr(data-value);
-      }
-      .content {
-        overflow: hidden;
-        width: var(--content-width, 80ch);
-      }
-      .content.left {
-        -webkit-user-select: var(--left-user-select, text);
-        -moz-user-select: var(--left-user-select, text);
-        -ms-user-select: var(--left-user-select, text);
-        user-select: var(--left-user-select, text);
-      }
-      .content.right {
-        -webkit-user-select: var(--right-user-select, text);
-        -moz-user-select: var(--right-user-select, text);
-        -ms-user-select: var(--right-user-select, text);
-        user-select: var(--right-user-select, text);
-      }
-      .add {
-        background-color: var(--dark-add-highlight-color);
-      }
-      .remove {
-        background-color: var(--dark-remove-highlight-color);
-      }
-      .contextControl,
-      .contextControl .lineNum {
-        color: #849;
-        background-color: #fef;
-      }
-      .contextControl gr-button {
-        font-family: var(--monospace-font-family);
-        text-decoration: none;
-      }
-      .contextControl td:not(.lineNum) {
-        text-align: center;
-      }
-      .br:after {
-        /* Line feed */
-        content: '\A';
-      }
-      .tab {
-        display: inline-block;
-      }
-      .tab.withIndicator:before {
-        color: #C62828;
-        /* >> character */
-        content: '\00BB';
-      }
-    </style>
-    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
-    <div hidden$="[[_loading]]" hidden>
-      <div class="header">
-        <gr-patch-range-select
-            path="[[path]]"
-            change-num="[[changeNum]]"
-            patch-range="[[patchRange]]"
-            available-patches="[[availablePatches]]"></gr-patch-range-select>
-        <gr-button link
-           class="prefsButton"
-           on-tap="_handlePrefsTap"
-           hidden$="[[!prefs]]"
-           hidden>Diff View Preferences</gr-button>
-      </div>
-      <gr-overlay id="prefsOverlay" with-backdrop>
-        <gr-diff-preferences
-            prefs="{{prefs}}"
-            on-save="_handlePrefsSave"
-            on-cancel="_handlePrefsCancel"></gr-diff-preferences>
-      </gr-overlay>
-
-      <div class="diffContainer"
-          on-tap="_handleTap"
-          on-mousedown="_handleMouseDown"
-          on-copy="_handleCopy">
-        <table id="diffTable"></table>
-      </div>
-    </div>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  </template>
-  <script src="gr-diff-line.js"></script>
-  <script src="gr-diff-group.js"></script>
-  <script src="gr-diff-builder.js"></script>
-  <script src="gr-diff-builder-side-by-side.js"></script>
-  <script src="gr-diff-builder-unified.js"></script>
-  <script src="gr-new-diff.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.js b/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.js
deleted file mode 100644
index 01f3027..0000000
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.js
+++ /dev/null
@@ -1,165 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-(function() {
-  'use strict';
-
-  var DiffViewMode = {
-    SIDE_BY_SIDE: 'SIDE_BY_SIDE',
-    UNIFIED: 'UNIFIED_DIFF',
-  };
-
-  var SelectionSide = {
-    LEFT: 'left',
-    RIGHT: 'right',
-  };
-
-  Polymer({
-    is: 'gr-new-diff',
-
-    properties: {
-      availablePatches: Array,
-      changeNum: String,
-      patchRange: Object,
-      path: String,
-      prefs: {
-        type: Object,
-        notify: true,
-      },
-      projectConfig: Object,
-
-      _loading: {
-        type: Boolean,
-        value: true,
-      },
-      _viewMode: {
-        type: String,
-        value: DiffViewMode.SIDE_BY_SIDE,
-      },
-      _diff: Object,
-      _diffBuilder: Object,
-      _selectionSide: {
-        type: String,
-        observer: '_selectionSideChanged',
-      },
-    },
-
-    observers: [
-      '_render(_diff, prefs.*)',
-    ],
-
-    reload: function() {
-      this.$.diffTable.innerHTML = null;
-      this._loading = true;
-
-      return this._getDiff().then(function(diff) {
-        this._diff = diff;
-        this._loading = false;
-      }.bind(this));
-    },
-
-    _handleTap: function(e) {
-      var el = Polymer.dom(e).rootTarget;
-      if (el.classList.contains('showContext')) {
-        this._showContext(e.detail.group, e.detail.section);
-      }
-    },
-
-    _handleMouseDown: function(e) {
-      var el = Polymer.dom(e).rootTarget;
-      var side;
-      for (var node = el; node != null; node = node.parentNode) {
-        if (node.classList.contains('left')) {
-          side = SelectionSide.LEFT;
-          break;
-        } else if (node.classList.contains('right')) {
-          side = SelectionSide.RIGHT;
-          break;
-        }
-      }
-      this._selectionSide = side;
-    },
-
-    _selectionSideChanged: function(side) {
-      if (side) {
-        var oppositeSide = side ==
-            SelectionSide.RIGHT ? SelectionSide.LEFT : SelectionSide.RIGHT;
-        this.customStyle['--' + side + '-user-select'] = 'text';
-        this.customStyle['--' + oppositeSide + '-user-select'] = 'none';
-      } else {
-        this.customStyle['--' + side + '-user-select'] = 'text';
-        this.customStyle['--' + oppositeSide + '-user-select'] = 'text';
-      }
-      this.updateStyles();
-    },
-
-    _handleCopy: function(e) {
-      var text = this._getSelectedText(this._selectionSide);
-      e.clipboardData.setData('Text', text);
-      e.preventDefault();
-    },
-
-    _getSelectedText: function(opt_side) {
-      var sel = window.getSelection();
-      var range = sel.getRangeAt(0);
-      var doc = range.cloneContents();
-      var selector = '.content';
-      if (opt_side) {
-        selector += '.' + opt_side
-      }
-      var contentEls = Polymer.dom(doc).querySelectorAll(selector);
-
-      if (contentEls.length === 0) {
-        return doc.textContent;
-      }
-
-      var text = '';
-      for (var i = 0; i < contentEls.length; i++) {
-        text += contentEls[i].textContent + '\n';
-      }
-      return text;
-    },
-
-    _showContext: function(group, sectionEl) {
-      this._builder.emitGroup(group, sectionEl);
-      sectionEl.parentNode.removeChild(sectionEl);
-    },
-
-    _render: function(diff, prefsChangeRecord) {
-      var prefs = prefsChangeRecord.base;
-      this.customStyle['--content-width'] = prefs.line_length + 'ch';
-      this.updateStyles();
-
-      this._builder = this._getDiffBuilder(diff, prefs);
-      this._builder.emitDiff(diff.content);
-    },
-
-    _getDiff: function() {
-      return this.$.restAPI.getDiff(
-          this.changeNum,
-          this.patchRange.basePatchNum,
-          this.patchRange.patchNum,
-          this.path);
-    },
-
-    _getDiffBuilder: function(diff, prefs) {
-      if (this._viewMode === DiffViewMode.SIDE_BY_SIDE) {
-        return new GrDiffBuilderSideBySide(diff, prefs, this.$.diffTable);
-      } else if (this._viewMode === DiffViewMode.UNIFIED) {
-        return new GrDiffBuilderUnified(diff, prefs, this.$.diffTable);
-      }
-      throw Error('Unsupported diff view mode: ' + this._viewMode);
-    },
-
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff_test.html b/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff_test.html
deleted file mode 100644
index a899d6b..0000000
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff_test.html
+++ /dev/null
@@ -1,48 +0,0 @@
-<!DOCTYPE html>
-<!--
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-new-diff</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-new-diff.html">
-
-<test-fixture id="basic">
-  <template>
-    <gr-new-diff></gr-new-diff>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-new-diff tests', function() {
-    var element;
-
-    setup(function() {
-      element = fixture('basic');
-    });
-
-    teardown(function() {
-    });
-
-    test('basic', function() {
-
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index b1c084e..33314a7 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -20,6 +20,7 @@
 
 <link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
 <link rel="import" href="./core/gr-main-header/gr-main-header.html">
+<link rel="import" href="./core/gr-router/gr-router.html">
 
 <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html">
 <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html">
@@ -29,8 +30,6 @@
 <link rel="import" href="./shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
-<script src="../bower_components/page/page.js"></script>
-<script src="../scripts/app.js"></script>
 <script src="../scripts/util.js"></script>
 
 <dom-module id="gr-app">
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 55d3920..cefce30 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -19,16 +19,6 @@
 
     properties: {
       params: Object,
-      accountReady: {
-        type: Object,
-        readOnly: true,
-        notify: true,
-        value: function() {
-          return new Promise(function(resolve) {
-            this._resolveAccountReady = resolve;
-          }.bind(this));
-        },
-      },
       keyEventTarget: {
         type: Object,
         value: function() { return document.body; },
@@ -42,7 +32,6 @@
       _version: String,
       _diffPreferences: Object,
       _preferences: Object,
-      _resolveAccountReady: Function,
       _showChangeListView: Boolean,
       _showDashboardView: Boolean,
       _showChangeView: Boolean,
@@ -98,8 +87,6 @@
     },
 
     _accountChanged: function(account) {
-      this._resolveAccountReady(account);
-
       if (this.loggedIn) {
         this.$.restAPI.getPreferences().then(function(preferences) {
           this._preferences = preferences;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
new file mode 100644
index 0000000..360c281
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -0,0 +1,66 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-account-link/gr-account-link.html">
+<link rel="import" href="../gr-button/gr-button.html">
+<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-account-chip">
+  <template>
+    <style>
+      :host {
+        display: block;
+        overflow: hidden;
+      }
+      .container {
+        align-items: center;
+        background: #eee;
+        border-radius: .75em;
+        display: inline-flex;
+        padding: 0 .5em;
+      }
+      :host([show-avatar]) .container {
+        padding-left: 0;
+      }
+      gr-button.remove,
+      gr-button.remove:hover,
+      gr-button.remove:focus {
+        border-color: transparent;
+        color: #333;
+      }
+      gr-button.remove {
+        background: #eee;
+        color: #666;
+        font-size: 1.7em;
+        font-weight: normal;
+        height: .6em;
+        line-height: .6em;
+        margin-left: .15em;
+        padding: 0;
+        text-decoration: none;
+      }
+    </style>
+    <div class="container">
+      <gr-account-link account="[[account]]"></gr-account-link>
+      <gr-button
+          hidden$="[[!removable]]" hidden
+          class="remove" on-tap="_handleRemoveTap">×</gr-button>
+    </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-account-chip.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
new file mode 100644
index 0000000..f5d7ee7
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -0,0 +1,50 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+(function() {
+  'use strict';
+
+  Polymer({
+    is: 'gr-account-chip',
+
+    properties: {
+      account: Object,
+      removable: {
+        type: Boolean,
+        value: false,
+      },
+      showAvatar: {
+        type: Boolean,
+        reflectToAttribute: true,
+      },
+    },
+
+    ready: function() {
+      this._getHasAvatars().then(function(hasAvatars) {
+        this.showAvatar = hasAvatars;
+      }.bind(this));
+    },
+
+    _handleRemoveTap: function(e) {
+      e.preventDefault();
+      this.fire('remove', {account: this.account}, {bubbles: false});
+    },
+
+    _getHasAvatars: function() {
+      return this.$.restAPI.getConfig().then(function(cfg) {
+        return Promise.resolve(!!(cfg && cfg.plugin && cfg.plugin.has_avatars));
+      });
+    },
+  });
+})();
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 af65bfd..eacd710 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,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.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 869d812..2b5b831 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,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.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_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index f065290..b308a38 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,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.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 03b8e13..8b13a86 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,6 @@
 <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="../../../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-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index a1b4a9b..3d0cf5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -26,7 +26,8 @@
         display: inline;
       }
     </style>
-    <span>[[_computeDateStr(dateStr, _timeFormat)]]</span>
+    <span title$="[[_computeFullDateStr(dateStr, _timeFormat)]]"
+        >[[_computeDateStr(dateStr, _timeFormat, _relative)]]</span>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-date-formatter.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index 8e9653c..57b4bd2 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -36,13 +36,48 @@
         notify: true,
       },
 
-      _timeFormat: String,
-      _timeFormatPromise: Object, // Used for testing.
+      _timeFormat: String, // No default value to prevent flickering.
+      _relative: Boolean, // No default value to prevent flickering.
     },
 
     attached: function() {
-      this._timeFormatPromise = this._getTimeFormat().then(function(tf) {
-        this._timeFormat = tf;
+      this._loadPreferences();
+    },
+
+    _loadPreferences: function() {
+      return this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) {
+          this._timeFormat = TimeFormats.TIME_24;
+          this._relative = false;
+          return;
+        }
+        return Promise.all([
+          this._loadTimeFormat(),
+          this._loadRelative(),
+        ]);
+      }.bind(this));
+    },
+
+    _loadTimeFormat: function() {
+      return this._getPreferences().then(function(preferences) {
+        var timeFormat = preferences && preferences.time_format;
+        switch (timeFormat) {
+          case 'HHMM_12':
+            this._timeFormat = TimeFormats.TIME_12;
+            break;
+          case 'HHMM_24':
+            this._timeFormat = TimeFormats.TIME_24;
+            break;
+          default:
+            throw Error('Invalid time format: ' + timeFormat);
+        }
+      }.bind(this));
+    },
+
+    _loadRelative: function() {
+      return this._getPreferences().then(function(prefs) {
+        // prefs.relative_date_in_change_table is not set when false.
+        this._relative = !!(prefs && prefs.relative_date_in_change_table);
       }.bind(this));
     },
 
@@ -54,16 +89,6 @@
       return this.$.restAPI.getPreferences();
     },
 
-    _getTimeFormat: function() {
-      return this._getLoggedIn().then(function(loggedIn) {
-        if (!loggedIn) { return 'HHMM_24'; }
-
-        return this._getPreferences().then(function(preferences) {
-          return preferences && preferences.time_format;
-        });
-      }.bind(this));
-    },
-
     /**
      * Return true if date is within 24 hours and on the same day.
      */
@@ -81,24 +106,28 @@
           diff < 180 * Duration.DAY;
     },
 
-    _computeDateStr: function(dateStr, timeFormat) {
+    _computeDateStr: function(dateStr, timeFormat, relative) {
       if (!dateStr) { return ''; }
       var date = moment(util.parseDate(dateStr));
       if (!date.isValid()) { return ''; }
+      if (relative) {
+        return date.fromNow();
+      }
       var now = new Date();
       var format = TimeFormats.MONTH_DAY_YEAR;
       if (this._isWithinDay(now, date)) {
-        if (timeFormat === 'HHMM_12') {
-          format = TimeFormats.TIME_12;
-        } else if (timeFormat === 'HHMM_24') {
-          format = TimeFormats.TIME_24;
-        } else {
-          throw Error('Invalid time format: ' + timeFormat);
-        }
+        format = timeFormat;
       } else if (this._isWithinHalfYear(now, date)) {
         format = TimeFormats.MONTH_DAY;
       }
       return date.format(format);
     },
+
+    _computeFullDateStr: function(dateStr, timeFormat) {
+      if (!dateStr) { return ''; }
+      var date = moment(util.parseDate(dateStr));
+      if (!date.isValid()) { return ''; }
+      return date.format(TimeFormats.MONTH_DAY_YEAR + ', ' +  timeFormat);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index bdfd22b..a77d4e7 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -43,83 +43,23 @@
       return d;
     }
 
-    function testDates(nowStr, dateStr, expected) {
+    function testDates(nowStr, dateStr, expected, expectedTooltip, done) {
       // Normalize and convert the date to mimic server response.
       dateStr = normalizedDate(dateStr)
           .toJSON().replace('T', ' ').slice(0, -1);
       var clock = sinon.useFakeTimers(normalizedDate(nowStr).getTime());
       element.dateStr = dateStr;
-      assert.equal(element.$$('span').textContent, expected);
-      clock.restore();
+      flush(function() {
+        var span = element.$$('span');
+        assert.equal(span.textContent, expected);
+        assert.equal(span.title, expectedTooltip);
+        clock.restore();
+        done();
+      });
     }
 
-    function stubGetTimeFormat(timeFormat) {
-      var timeFormatPromise = Promise.resolve(timeFormat);
-      stub('gr-date-formatter', {
-        _getTimeFormat: sinon.stub().returns(timeFormatPromise),
-      });
-      return timeFormatPromise;
-    }
-
-    suite('24 hours time format preference', function() {
-      setup(function() {
-        return stubGetTimeFormat('HHMM_24').then(function() {
-          element = fixture('basic');
-        });
-      });
-
-      test('invalid dates are quietly rejected', function() {
-        assert.notOk((new Date('foo')).valueOf());
-        assert.equal(element._computeDateStr('foo', 'HHMM_12'), '');
-      });
-
-      test('Within 24 hours on same day', function() {
-        testDates('2015-07-29 20:34:14.985000000',
-                  '2015-07-29 15:34:14.985000000',
-                  '15:34');
-        testDates('2016-01-27 17:41:14.985000000',
-                  '2016-01-27 12:41:14.985000000',
-                  '12:41');
-      });
-
-      test('Within 24 hours on different days', function() {
-        testDates('2015-07-29 03:34:14.985000000',
-                  '2015-07-28 20:25:14.985000000',
-                  'Jul 28');
-      });
-
-      test('More than 24 hours but less than six months', function() {
-        testDates('2015-07-29 20:34:14.985000000',
-                  '2015-06-15 03:25:14.985000000',
-                  'Jun 15');
-      });
-
-      test('More than six months', function() {
-        testDates('2015-09-15 20:34:00.000000000',
-                  '2015-01-15 03:25:00.000000000',
-                  'Jan 15, 2015');
-      });
-    });
-
-    suite('12 hours time format preference', function() {
-      setup(function() {
-        return stubGetTimeFormat('HHMM_12').then(function() {
-          element = fixture('basic');
-        });
-      });
-
-      test('Within 24 hours on same day', function() {
-        testDates('2015-07-29 20:34:14.985000000',
-                  '2015-07-29 15:34:14.985000000',
-                  '3:34 PM');
-        testDates('2016-01-27 17:41:14.985000000',
-                  '2016-01-27 12:41:14.985000000',
-                  '12:41 PM');
-      });
-    });
-
-    function stubRestAPI(loggedIn, preferences) {
-      var loggedInPromise = Promise.resolve(loggedIn);
+    function stubRestAPI(preferences) {
+      var loggedInPromise = Promise.resolve(preferences !== null);
       var preferencesPromise = Promise.resolve(preferences);
       stub('gr-rest-api-interface', {
         getLoggedIn: sinon.stub().returns(loggedInPromise),
@@ -128,17 +68,99 @@
       return Promise.all([loggedInPromise, preferencesPromise]);
     }
 
+    suite('24 hours time format preference', function() {
+      setup(function() {
+        return stubRestAPI(
+          {time_format: 'HHMM_24', relative_date_in_change_table: false}
+        ).then(function() {
+          element = fixture('basic');
+        });
+      });
+
+      test('invalid dates are quietly rejected', function() {
+        assert.notOk((new Date('foo')).valueOf());
+        assert.equal(element._computeDateStr('foo', 'h:mm A'), '');
+      });
+
+      test('Within 24 hours on same day', function(done) {
+        testDates('2015-07-29 20:34:14.985000000',
+                  '2015-07-29 15:34:14.985000000',
+                  '15:34', 'Jul 29, 2015, 15:34', done);
+      });
+
+      test('Within 24 hours on different days', function(done) {
+        testDates('2015-07-29 03:34:14.985000000',
+                  '2015-07-28 20:25:14.985000000',
+                  'Jul 28', 'Jul 28, 2015, 20:25', done);
+      });
+
+      test('More than 24 hours but less than six months', function(done) {
+        testDates('2015-07-29 20:34:14.985000000',
+                  '2015-06-15 03:25:14.985000000',
+                  'Jun 15', 'Jun 15, 2015, 3:25', done);
+      });
+
+      test('More than six months', function(done) {
+        testDates('2015-09-15 20:34:00.000000000',
+                  '2015-01-15 03:25:00.000000000',
+                  'Jan 15, 2015', 'Jan 15, 2015, 3:25', done);
+      });
+    });
+
+    suite('12 hours time format preference', function() {
+      setup(function() {
+        // relative_date_in_change_table is not set when false.
+        return stubRestAPI(
+          {time_format: 'HHMM_12'}
+        ).then(function() {
+          element = fixture('basic');
+        });
+      });
+
+      test('Within 24 hours on same day', function(done) {
+        testDates('2015-07-29 20:34:14.985000000',
+                  '2015-07-29 15:34:14.985000000',
+                  '3:34 PM', 'Jul 29, 2015, 3:34 PM', done);
+      });
+    });
+
+    suite('relative date preference', function() {
+      setup(function(done) {
+        return stubRestAPI(
+          {time_format: 'HHMM_12', relative_date_in_change_table: true}
+        ).then(function() {
+          element = fixture('basic');
+          done();
+        });
+      });
+
+      test('Within 24 hours on same day', function(done) {
+        testDates('2015-07-29 20:34:14.985000000',
+                  '2015-07-29 15:34:14.985000000',
+                  '5 hours ago', 'Jul 29, 2015, 3:34 PM', done);
+      });
+
+      test('More than six months', function(done) {
+        testDates('2015-09-15 20:34:00.000000000',
+                  '2015-01-15 03:25:00.000000000',
+                  '8 months ago', 'Jan 15, 2015, 3:25 AM', done);
+      });
+    });
+
     suite('logged in', function() {
       setup(function(done) {
-        return stubRestAPI(true, {time_format: 'HHMM_12'}).then(function() {
+        return stubRestAPI(
+          {time_format: 'HHMM_12', relative_date_in_change_table: true}
+        ).then(function() {
           element = fixture('basic');
           done();
         });
       });
 
       test('Preferences are respected', function(done) {
-        element._timeFormatPromise.then(function() {
-          assert.equal(element._timeFormat, 'HHMM_12');
+        element._loadPreferences().then(function() {
+          assert.equal(element._timeFormat, 'h:mm A');
+          assert.isTrue(element._relative);
           done();
         });
       });
@@ -146,15 +168,16 @@
 
     suite('logged out', function() {
       setup(function(done) {
-        return stubRestAPI(false, null).then(function() {
+        return stubRestAPI(null).then(function() {
           element = fixture('basic');
           done();
         });
       });
 
       test('Default preferences are respected', function(done) {
-        element._timeFormatPromise.then(function() {
-          assert.equal(element._timeFormat, 'HHMM_24');
+        element._loadPreferences().then(function() {
+          assert.equal(element._timeFormat, 'H:mm');
+          assert.isFalse(element._relative);
           done();
         });
       });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
new file mode 100644
index 0000000..a3b489d
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -0,0 +1,23 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-js-api-interface">
+  <template></template>
+  <script src="gr-js-api-interface.js"></script>
+  <script src="gr-public-js-api.js"></script>
+</dom-module>
+
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
new file mode 100644
index 0000000..d87d572
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -0,0 +1,107 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  var EventType = {
+    HISTORY: 'history',
+    SHOW_CHANGE: 'showchange',
+    SUBMIT_CHANGE: 'submitchange',
+    COMMENT: 'comment',
+  };
+
+  Polymer({
+    is: 'gr-js-api-interface',
+
+    properties: {
+      _eventCallbacks: {
+        type: Object,
+        value: {},  // Shared across all instances.
+      },
+    },
+
+    EventType: EventType,
+
+    handleEvent: function(type, detail) {
+      switch (type) {
+        case EventType.HISTORY:
+          this._handleHistory(detail);
+          break;
+        case EventType.SHOW_CHANGE:
+          this._handleShowChange(detail);
+          break;
+        case EventType.COMMENT:
+          this._handleComment(detail);
+          break;
+        default:
+          console.warn('handleEvent called with unsupported event type:', type);
+          break;
+      }
+    },
+
+    addEventCallback: function(eventName, callback) {
+      if (!this._eventCallbacks[eventName]) {
+        this._eventCallbacks[eventName] = [];
+      }
+      this._eventCallbacks[eventName].push(callback);
+    },
+
+    canSubmitChange: function() {
+      var submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE);
+
+      var cancelSubmit = submitCallbacks.some(function(callback) {
+        return callback() === false;
+      });
+
+      return !cancelSubmit;
+    },
+
+    _removeEventCallbacks: function() {
+      for (var k in EventType) {
+        this._eventCallbacks[EventType[k]] = [];
+      }
+    },
+
+    _handleHistory: function(detail) {
+      this._getEventCallbacks(EventType.HISTORY).forEach(function(cb) {
+        cb(detail.path);
+      });
+    },
+
+    _handleShowChange: function(detail) {
+      this._getEventCallbacks(EventType.SHOW_CHANGE).forEach(function(cb) {
+        var change = detail.change;
+        var patchNum = detail.patchNum
+        var revision;
+        for (var rev in change.revisions) {
+          if (change.revisions[rev]._number == patchNum) {
+            revision = change.revisions[rev];
+            break;
+          }
+        }
+        cb(change, revision);
+      });
+    },
+
+    _handleComment: function(detail) {
+      this._getEventCallbacks(EventType.COMMENT).forEach(function(cb) {
+        cb(detail.node);
+      });
+    },
+
+    _getEventCallbacks: function(type) {
+      return this._eventCallbacks[type] || [];
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
new file mode 100644
index 0000000..2fede7c
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -0,0 +1,90 @@
+<!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-api-interface</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="gr-js-api-interface.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-js-api-interface></gr-js-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-js-api-interface tests', function() {
+    var element;
+    var plugin;
+
+    setup(function() {
+      element = fixture('basic');
+      Gerrit.install(function(p) { plugin = p; });
+    });
+
+    teardown(function() {
+      element._removeEventCallbacks();
+      plugin = null;
+    });
+
+    test('history event', function(done) {
+      plugin.on(element.EventType.HISTORY, function(path) {
+        assert.equal(path, '/path/to/awesomesauce');
+        done();
+      });
+      element.handleEvent(element.EventType.HISTORY,
+          {path: '/path/to/awesomesauce'});
+    });
+
+    test('showchange event', function(done) {
+      var testChange = {
+        _number: 42,
+        revisions: {
+          def: { _number: 2 },
+          abc: { _number: 1 },
+        },
+      };
+      plugin.on(element.EventType.SHOW_CHANGE, function(change, revision) {
+        assert.deepEqual(change, testChange);
+        assert.deepEqual(revision, testChange.revisions.abc);
+        done();
+      });
+      element.handleEvent(element.EventType.SHOW_CHANGE,
+          {change: testChange, patchNum: 1});
+    });
+
+    test('comment event', function(done) {
+      var testCommentNode = { foo: 'bar' };
+      plugin.on(element.EventType.COMMENT, function(commentNode) {
+        assert.deepEqual(commentNode, testCommentNode);
+        done();
+      });
+      element.handleEvent(element.EventType.COMMENT, {node: testCommentNode});
+    });
+
+    test('submitchange', function() {
+      plugin.on(element.EventType.SUBMIT_CHANGE, function() { return true; });
+      assert.isTrue(element.canSubmitChange());
+      plugin.on(element.EventType.SUBMIT_CHANGE, function() { return false; });
+      plugin.on(element.EventType.SUBMIT_CHANGE, function() { return true; });
+      assert.isFalse(element.canSubmitChange());
+    });
+
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
new file mode 100644
index 0000000..b5c84d3
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -0,0 +1,43 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function(window) {
+  'use strict';
+
+  function Plugin() {}
+
+  Plugin.prototype.on = function(eventName, callback) {
+    document.createElement('gr-js-api-interface').addEventCallback(eventName,
+        callback);
+  };
+
+  var Gerrit = window.Gerrit || {};
+
+  Gerrit.css = function(rulesStr) {
+    if (!Gerrit._customStyleSheet) {
+      var styleEl = document.createElement('style');
+      document.head.appendChild(styleEl);
+      Gerrit._customStyleSheet = styleEl.sheet;
+    }
+
+    var name = '__pg_js_api_class_' + Gerrit._customStyleSheet.cssRules.length;
+    Gerrit._customStyleSheet.insertRule('.' + name + '{' + rulesStr + '}', 0);
+    return name;
+  },
+
+  Gerrit.install = function(callback) {
+    callback(new Plugin());
+  };
+
+  window.Gerrit = Gerrit;
+})(window);
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 37bca2e..79db969 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
@@ -25,7 +25,7 @@
       }
       :host([pre]) span {
         white-space: var(--linked-text-white-space, pre-wrap);
-        word-wrap: var(--linked-text-work-wrap, break-word);
+        word-wrap: var(--linked-text-word-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 14c3497..477fafe 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
@@ -94,6 +94,11 @@
       return this._fetchSharedCacheURL('/accounts/self/preferences.diff');
     },
 
+    saveDiffPreferences: function(prefs, opt_errFn, opt_ctx) {
+      return this._save('PUT', '/accounts/self/preferences.diff', prefs,
+          opt_errFn, opt_ctx);
+    },
+
     getAccount: function() {
       return this._fetchSharedCacheURL('/accounts/self/detail');
     },
@@ -160,6 +165,9 @@
       };
       if (opt_body) {
         headers.append('Content-Type', 'application/json');
+        if (typeof opt_body !== 'string') {
+          opt_body = JSON.stringify(opt_body);
+        }
         options.body = opt_body;
       }
       return fetch(url, options).catch(function(err) {
diff --git a/polygerrit-ui/app/scripts/app.js b/polygerrit-ui/app/scripts/app.js
deleted file mode 100644
index 1922311..0000000
--- a/polygerrit-ui/app/scripts/app.js
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-'use strict';
-
-// Polymer makes `app` intrinsically defined on the window by virtue of the
-// custom element having the id "app", but it is made explicit here.
-var app = document.querySelector('#app');
-
-window.addEventListener('WebComponentsReady', function() {
-  // Middleware
-  page(function(ctx, next) {
-    document.body.scrollTop = 0;
-    next();
-  });
-
-  function loadUser(ctx, next) {
-    app.accountReady.then(function() {
-      next();
-    });
-  }
-
-  // Routes.
-  page('/', loadUser, function(data) {
-    // For backward compatibility with GWT links.
-    if (data.hash) {
-      page.redirect(data.hash);
-      return;
-    }
-    if (app.loggedIn) {
-      page.redirect('/dashboard/self');
-    } else {
-      page.redirect('/q/status:open');
-    }
-  });
-
-  page('/dashboard/(.*)', loadUser, function(data) {
-    if (app.loggedIn) {
-      data.params.view = 'gr-dashboard-view';
-      app.params = data.params;
-    } else {
-      page.redirect('/login/' + encodeURIComponent(data.canonicalPath));
-    }
-  });
-
-  function queryHandler(data) {
-    data.params.view = 'gr-change-list-view';
-    app.params = data.params;
-  }
-
-  page('/q/:query,:offset', queryHandler);
-  page('/q/:query', queryHandler);
-
-  page(/^\/(\d+)\/?/, function(ctx) {
-    page.redirect('/c/' + ctx.params[0]);
-  });
-
-  page('/c/:changeNum/:patchNum?', function(data) {
-    data.params.view = 'gr-change-view';
-    app.params = data.params;
-  });
-
-  page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, function(ctx) {
-    var params = {
-      changeNum: ctx.params[0],
-      basePatchNum: ctx.params[2],
-      patchNum: ctx.params[4],
-      path: ctx.params[5],
-      view: 'gr-diff-view',
-    };
-    // Don't allow diffing the same patch number against itself because WHY?
-    if (params.basePatchNum == params.patchNum) {
-      page.redirect('/c/' + params.changeNum + '/' + params.patchNum + '/' +
-          params.path);
-      return;
-    }
-    if (!params.patchNum) {
-      params.patchNum = params.basePatchNum;
-      delete(params.basePatchNum);
-    }
-    app.params = params;
-  });
-
-  page.start();
-});
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 3f806a4..13f3243 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -11,46 +11,49 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+(function(window) {
+  'use strict';
 
-'use strict';
+  var util = window.util || {};
 
-var util = util || {};
+  util.parseDate = function(dateStr) {
+    // Timestamps are given in UTC and have the format
+    // "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
+    // nanoseconds.
+    // Munge the date into an ISO 8061 format and parse that.
+    return new Date(dateStr.replace(' ', 'T') + 'Z');
+  };
 
-util.parseDate = function(dateStr) {
-  // Timestamps are given in UTC and have the format
-  // "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
-  // nanoseconds.
-  // Munge the date into an ISO 8061 format and parse that.
-  return new Date(dateStr.replace(' ', 'T') + 'Z');
-};
+  util.htmlEntityMap = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    '\'': '&#39;',
+    '/': '&#x2F;',
+    '`': '&#96;',
+  };
 
-util.htmlEntityMap = {
-  '&': '&amp;',
-  '<': '&lt;',
-  '>': '&gt;',
-  '"': '&quot;',
-  '\'': '&#39;',
-  '/': '&#x2F;',
-  '`': '&#96;',
-};
+  util.escapeHTML = function(str) {
+    return str.replace(/[&<>"'`\/]/g, function(s) {
+      return util.htmlEntityMap[s];
+    });
+  };
 
-util.escapeHTML = function(str) {
-  return str.replace(/[&<>"'`\/]/g, function(s) {
-    return util.htmlEntityMap[s];
-  });
-};
-
-util.getCookie = function(name) {
-  var key = name + '=';
-  var cookies = document.cookie.split(';');
-  for (var i = 0; i < cookies.length; i++) {
-    var c = cookies[i];
-    while (c.charAt(0) == ' ') {
-      c = c.substring(1);
+  util.getCookie = function(name) {
+    var key = name + '=';
+    var cookies = document.cookie.split(';');
+    for (var i = 0; i < cookies.length; i++) {
+      var c = cookies[i];
+      while (c.charAt(0) == ' ') {
+        c = c.substring(1);
+      }
+      if (c.indexOf(key) == 0) {
+        return c.substring(key.length, c.length);
+      }
     }
-    if (c.indexOf(key) == 0) {
-      return c.substring(key.length, c.length);
-    }
-  }
-  return '';
-};
+    return '';
+  };
+
+  window.util = util;
+})(window);
diff --git a/polygerrit-ui/app/styles/fonts.css b/polygerrit-ui/app/styles/fonts.css
new file mode 100644
index 0000000..30c6e2e
--- /dev/null
+++ b/polygerrit-ui/app/styles/fonts.css
@@ -0,0 +1,160 @@
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Open Sans'), local('OpenSans'),
+       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
+       url(../fonts/OpenSans-Regular.woff) format('woff');
+  unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
+}
+/* cyrillic */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Open Sans'), local('OpenSans'),
+       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
+       url(../fonts/OpenSans-Regular.woff) format('woff');
+  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Open Sans'), local('OpenSans'),
+       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
+       url(../fonts/OpenSans-Regular.woff) format('woff');
+  unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Open Sans'), local('OpenSans'),
+       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
+       url(../fonts/OpenSans-Regular.woff) format('woff');
+  unicode-range: U+0370-03FF;
+}
+/* vietnamese */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Open Sans'), local('OpenSans'),
+       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
+       url(../fonts/OpenSans-Regular.woff) format('woff');
+  unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Open Sans'), local('OpenSans'),
+       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
+       url(../fonts/OpenSans-Regular.woff) format('woff');
+  unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Open Sans'), local('OpenSans'),
+       url(../fonts/OpenSans-Regular.woff2) format('woff2'),
+       url(../fonts/OpenSans-Regular.woff) format('woff');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
+}
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 700;
+  src: local('Open Sans Bold'), local('OpenSans-Bold'),
+       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
+       url(../fonts/OpenSans-Bold.woff) format('woff');
+  unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
+}
+/* cyrillic */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 700;
+  src: local('Open Sans Bold'), local('OpenSans-Bold'),
+       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
+       url(../fonts/OpenSans-Bold.woff) format('woff');
+  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 700;
+  src: local('Open Sans Bold'), local('OpenSans-Bold'),
+       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
+       url(../fonts/OpenSans-Bold.woff) format('woff');
+  unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 700;
+  src: local('Open Sans Bold'), local('OpenSans-Bold'),
+       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
+       url(../fonts/OpenSans-Bold.woff) format('woff');
+  unicode-range: U+0370-03FF;
+}
+/* vietnamese */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 700;
+  src: local('Open Sans Bold'), local('OpenSans-Bold'),
+       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
+       url(../fonts/OpenSans-Bold.woff) format('woff');
+  unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 700;
+  src: local('Open Sans Bold'), local('OpenSans-Bold'),
+       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
+       url(../fonts/OpenSans-Bold.woff) format('woff');
+  unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Open Sans';
+  font-style: normal;
+  font-weight: 700;
+  src: local('Open Sans Bold'), local('OpenSans-Bold'),
+       url(../fonts/OpenSans-Bold.woff2) format('woff2'),
+       url(../fonts/OpenSans-Bold.woff) format('woff');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Source Code Pro';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Source Code Pro'), local('SourceCodePro-Regular'),
+       url(../fonts/SourceCodePro-Regular.woff2) format('woff2'),
+       url(../fonts/SourceCodePro-Regular.woff) format('woff');
+  unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Source Code Pro';
+  font-style: normal;
+  font-weight: 400;
+  src: local('Source Code Pro'), local('SourceCodePro-Regular'),
+       url(../fonts/SourceCodePro-Regular.woff2) format('woff2'),
+       url(../fonts/SourceCodePro-Regular.woff) format('woff');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
+}
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index 5179ac2..b4cc415 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -68,7 +68,7 @@
         width: 7em;
       }
       .updated {
-        width: 6em;
+        width: 9em;
         text-align: right;
       }
       .size {
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css
index aee49d3..5c5ad96 100644
--- a/polygerrit-ui/app/styles/main.css
+++ b/polygerrit-ui/app/styles/main.css
@@ -14,8 +14,7 @@
 limitations under the License.
 */
 
-@import "//fonts.googleapis.com/css?family=Open+Sans:400,700";
-@import "//fonts.googleapis.com/css?family=Source+Code+Pro";
+@import "fonts.css";
 
 *,
 *::after,
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 5da0ace..4238f9a 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -38,16 +38,15 @@
     '../elements/change-list/gr-change-list/gr-change-list_test.html',
     '../elements/change-list/gr-change-list-item/gr-change-list-item_test.html',
     '../elements/core/gr-account-dropdown/gr-account-dropdown_test.html',
+    '../elements/core/gr-main-header/gr-main-header_test.html',
     '../elements/core/gr-search-bar/gr-search-bar_test.html',
+    '../elements/diff/gr-diff/gr-diff-builder_test.html',
+    '../elements/diff/gr-diff/gr-diff-group_test.html',
     '../elements/diff/gr-diff/gr-diff_test.html',
     '../elements/diff/gr-diff-comment/gr-diff-comment_test.html',
     '../elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
     '../elements/diff/gr-diff-preferences/gr-diff-preferences_test.html',
-    '../elements/diff/gr-diff-side/gr-diff-side_test.html',
     '../elements/diff/gr-diff-view/gr-diff-view_test.html',
-    '../elements/diff/gr-new-diff/gr-diff-builder_test.html',
-    '../elements/diff/gr-new-diff/gr-diff-group_test.html',
-    '../elements/diff/gr-new-diff/gr-new-diff_test.html',
     '../elements/diff/gr-patch-range-select/gr-patch-range-select_test.html',
     '../elements/shared/gr-account-label/gr-account-label_test.html',
     '../elements/shared/gr-account-link/gr-account-link_test.html',
@@ -55,6 +54,7 @@
     '../elements/shared/gr-change-star/gr-change-star_test.html',
     '../elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
     '../elements/shared/gr-date-formatter/gr-date-formatter_test.html',
+    '../elements/shared/gr-js-api-interface/gr-js-api-interface_test.html',
     '../elements/shared/gr-linked-text/gr-linked-text_test.html',
     '../elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
   ].forEach(function(file) {
diff --git a/polygerrit-ui/run-server.sh b/polygerrit-ui/run-server.sh
index 70ee3cb..e6d782f 100755
--- a/polygerrit-ui/run-server.sh
+++ b/polygerrit-ui/run-server.sh
@@ -23,8 +23,14 @@
   exit 1
 fi
 
-cd polygerrit-ui
+buck build \
+  //polygerrit-ui/app:test_components \
+  //polygerrit-ui:fonts
+
+cd polygerrit-ui/app
 rm -rf bower_components
-buck build //polygerrit-ui:polygerrit_components
-unzip -q ../buck-out/gen/polygerrit-ui/polygerrit_components/polygerrit_components.bower_components.zip
-exec go run server.go
+unzip -q ../../buck-out/gen/polygerrit-ui/app/test_components/test_components.bower_components.zip
+rm -rf fonts
+unzip -q ../../buck-out/gen/polygerrit-ui/fonts/fonts.zip -d fonts
+cd ..
+exec go run server.go "$@"
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index a7ec44c..66f9e55 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -30,7 +30,7 @@
 )
 
 var (
-	restHost = flag.String("host", "canary-chromium-review.googlesource.com", "Host to proxy requests to")
+	restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
 	port     = flag.String("port", ":8081", "Port to serve HTTP requests on")
 	prod     = flag.Bool("prod", false, "Serve production assets")
 	loggedIn = flag.Bool("logged_in", false, "Return user info as if the user is logged in")
@@ -42,8 +42,6 @@
 	if *prod {
 		http.Handle("/", http.FileServer(http.Dir("dist")))
 	} else {
-		http.Handle("/bower_components/",
-			http.StripPrefix("/bower_components/", http.FileServer(http.Dir("bower_components"))))
 		http.Handle("/", http.FileServer(http.Dir("app")))
 	}