Merge branch 'stable-2.13'

* stable-2.13:
  Add es6-promise for Internet Explorer

Change-Id: I72f323e79332054d53aeb4548d83145bdee28cd8
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index eeb1b30..d48034a 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -466,7 +466,10 @@
 
 To push lightweight (non-annotated) tags, grant
 `Create Reference` for reference name `+refs/tags/*+`, as lightweight
-tags are implemented just like branches in Git.
+tags are implemented just like branches in Git. To push a lightweight
+tag on a new commit (commit not reachable from any branch/tag) grant
+`Push` permission on `+refs/tags/*+` too. The `Push` permission on
+`+refs/tags/*+` also allows fast-forwarding of lightweight tags.
 
 For example, to grant the possibility to create new branches under the
 namespace `foo`, you have to grant this permission on
@@ -480,6 +483,19 @@
 you grant the users the push force permission to be able to clean up
 stale branches.
 
+[[category_delete]]
+=== Delete Reference
+
+The delete reference category controls whether it is possible to delete
+references, branches or tags. It doesn't allow any other update of
+references.
+
+Deletion of references is also possible if `Push` with the force option
+is granted, however that includes the permission to fast-forward and
+force-update references to exiting and new commits. Being able to push
+references for new commits is bad if bypassing of code review must be
+prevented.
+
 
 [[category_forge_author]]
 === Forge Author
@@ -644,7 +660,8 @@
 
 
 [[category_push_annotated]]
-=== Push Annotated Tag
+[[category_create_annotated]]
+=== Create Annotated Tag
 
 This category permits users to push an annotated tag object into the
 project's repository.  Typically this would be done with a command line
@@ -671,7 +688,7 @@
 
 To push tags created by users other than the current user (such
 as tags mirrored from an upstream project), `Forge Committer Identity`
-must be also granted in addition to `Push Annotated Tag`.
+must be also granted in addition to `Create Annotated Tag`.
 
 To push lightweight (non annotated) tags, grant
 <<category_create,`Create Reference`>> for reference name
@@ -682,9 +699,16 @@
 option enabled for reference name `+refs/tags/*+`, as deleting a tag
 requires the same permission as deleting a branch.
 
+To push an annotated tag on a new commit (commit not reachable from any
+branch/tag) grant `Push` permission on `+refs/tags/*+` too.
+The `Push` permission on `+refs/tags/*+` does *not* allow updating of annotated
+tags, not even fast-forwarding of annotated tags. Update of annotated tags
+is only allowed by granting `Push` with `force` option on `+refs/tags/*+`.
+
 
 [[category_push_signed]]
-=== Push Signed Tag
+[[category_create_signed]]
+=== Create Signed Tag
 
 This category permits users to push a PGP signed tag object into the
 project's repository.  Typically this would be done with a command
@@ -997,7 +1021,7 @@
 * <<category_push_merge,`Push merge commit`>> to 'refs/heads/*'
 * <<category_forge_committer,`Forge Committer Identity`>> to 'refs/for/refs/heads/*'
 * <<category_create,`Create Reference`>> to 'refs/heads/*'
-* <<category_push_annotated,`Push Annotated Tag`>> to 'refs/tags/*'
+* <<category_create_annotated,`Create Annotated Tag`>> to 'refs/tags/*'
 
 
 [[examples_project-owner]]
@@ -1067,12 +1091,15 @@
 [[block]]
 === 'BLOCK' access rule
 
-The 'BLOCK' rule blocks a permission globally. An inherited 'BLOCK' rule cannot
-be overridden in the inheriting project. Any 'ALLOW' rule, from a different
-access section or from an inheriting project, which conflicts with an
-inherited 'BLOCK' rule will not be honored.  Searching for 'BLOCK' rules, in
-the chain of parent projects, ignores the Exclusive flag that is normally
-applied to access sections.
+The 'BLOCK' rule blocks a permission globally. An inherited 'BLOCK'
+rule cannot be overridden in the inheriting project. Any 'ALLOW' rule
+from an inheriting project, which conflicts with an inherited 'BLOCK'
+rule will not be honored. Searching for 'BLOCK' rules, in the chain
+of parent projects, ignores the Exclusive flag, unless the rule with
+the Exclusive flag is defined on the same project as the 'BLOCK'
+rule. This means within the same project a 'BLOCK' rule can be
+overruled by 'ALLOW' rules on the same access section and 'ALLOW'
+rules with Exclusive flag on access section for more specific refs.
 
 A 'BLOCK' rule that blocks the 'push' permission blocks any type of push,
 force or not. A blocking force push rule blocks only force pushes, but
diff --git a/Documentation/config-cla.txt b/Documentation/config-cla.txt
index c07a24f..2234808 100644
--- a/Documentation/config-cla.txt
+++ b/Documentation/config-cla.txt
@@ -37,8 +37,13 @@
 Each `contributor-agreement` section within the `project.config` file must
 have a unique name. The section name will appear in the web UI.
 
-If not already present, add the UUID of the groups used in the
-`autoVerify` and `accepted` variables in the groups file.
+If not already present, add the group(s) used in the `autoVerify` and
+`accepted` variables in the `groups` file:
+----
+    # UUID                                  	Group Name
+    #
+    3dedb32915ecdbef5fced9f0a2587d164cd614d4	CLA Accepted - Individual
+----
 
 Commit the configuration change, and push it back:
 ----
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 1c08a68..bb112a5 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3331,7 +3331,9 @@
 Full Name and Preferred Email.  This may cause messages to be
 classified as spam if the user's domain has SPF or DKIM enabled
 and <<sendemail.smtpServer,sendemail.smtpServer>> is not a trusted
-relay for that domain.
+relay for that domain. You can specify
+<<sendemail.allowedDomain,sendemail.allowedDomain>> to instruct Gerrit to only
+send as USER if USER is from those domains.
 +
 * `MIXED`
 +
@@ -3357,6 +3359,16 @@
 +
 By default, MIXED.
 
+[[sendemail.allowedDomain]]sendemail.allowedDomain::
++
+Only used when `sendemail.from` is set to `USER`.
+List of allowed domains. If user's email matches one of the domains, emails will
+be sent as USER, otherwise as MIXED mode. Wildcards may be specified by
+including `*` to match any number of characters, for example `*.example.com`
+matches any subdomain of `example.com`.
++
+By default, `*`.
+
 [[sendemail.smtpServer]]sendemail.smtpServer::
 +
 Hostname (or IP address) of a SMTP server that will relay
@@ -3442,6 +3454,15 @@
 [[site]]
 === Section site
 
+[[site.allowOriginRegex]]site.allowOriginRegex::
++
+List of regular expressions matching origins that should be permitted
+to use the Gerrit REST API to read content. These should be trusted
+applications as the sites may be able to use the user's credentials.
+Only applies to GET and HEAD requests.
++
+By default, unset, denying all cross-origin requests.
+
 [[site.refreshHeaderFooter]]site.refreshHeaderFooter::
 +
 If true the server checks the site header, footer and CSS files for
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 7121265..5483d85 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -20,6 +20,11 @@
 that you will have to configure push rights for the +refs/meta/config+ name
 space if you'd like to use the possibility to automate permission updates.
 
+== Property inheritance
+
+If a property is set to INHERIT, then the value from the parent project is
+used. If the property is not set in any parent project, the default value is
+FALSE.
 
 [[file-project_config]]
 == The file +project.config+
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 71ed460..ecea83f 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -36,7 +36,7 @@
 ----
 mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \
     -DarchetypeArtifactId=gerrit-plugin-archetype \
-    -DarchetypeVersion=2.13-SNAPSHOT \
+    -DarchetypeVersion=2.14-SNAPSHOT \
     -DgroupId=com.googlesource.gerrit.plugins.testplugin \
     -DartifactId=testplugin
 ----
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
index 921244f..900e95c 100644
--- a/Documentation/dev-release-deploy-config.txt
+++ b/Documentation/dev-release-deploy-config.txt
@@ -143,10 +143,9 @@
 ----
 
 [NOTE]
-In case of JGit the `pom.xml` already contains a distributionManagement
-section.  Replace the existing distributionManagement section with this snippet
-in order to deploy the artifacts only in the gerrit-maven repository.
-
+In case of JGit the `pom.xml` already contains a `distributionManagement`
+section.  To deploy the artifacts to the `gerrit-maven` repository, replace
+the existing `distributionManagement` section with this snippet.
 
 * Add these two snippets to the `pom.xml` to enable the wagon provider:
 
diff --git a/Documentation/dev-release-jgit.txt b/Documentation/dev-release-jgit.txt
index f6d4d68..1a8b501 100644
--- a/Documentation/dev-release-jgit.txt
+++ b/Documentation/dev-release-jgit.txt
@@ -1,33 +1,44 @@
-= Making a Release of JGit
+= Making a Snapshot Release of JGit
 
 This step is only necessary if we need to create an unofficial JGit
 snapshot release and publish it to the
 link:https://developers.google.com/storage/[Google Cloud Storage].
 
+[[prepare-environment]]
+== Prepare the Maven Environment
+
+First, make sure you have done the necessary
+link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
+configuration in Maven `settings.xml`].
+
+To apply the necessary settings in JGit's `pom.xml`, follow the instructions
+in link:dev-release-deploy-config.html#deploy-configuration-subprojects[
+Configuration for Subprojects in `pom.xml`], or apply the provided diff by
+executing the following command in the JGit workspace:
+
+----
+  git apply /path/to/gerrit/tools/jgit-snapshot-deploy-pom.diff
+----
 
 [[prepare-release]]
 == Prepare the Release
 
-Since JGit has its own release process we do not push any release tags
-for JGit. Instead we will use the output of the `git describe` as the
-version of the current JGit snapshot.
+Since JGit has its own release process we do not push any release tags. Instead
+we will use the output of `git describe` as the version of the current JGit
+snapshot.
+
+In the JGit workspace, execute the following command:
 
 ----
   ./tools/version.sh --release $(git describe)
 ----
 
-
 [[publish-release]]
 == Publish the Release
 
-* Make sure you have done the configuration needed for deployment:
-** link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
-Configuration in Maven `settings.xml`]
-** link:dev-release-deploy-config.html#deploy-configuration-subprojects[
-Configuration for Subprojects in `pom.xml`]
+To deploy the new snapshot, execute the following command in the JGit
+workspace:
 
-* Deploy the new snapshot. From JGit workspace execute:
-+
 ----
   mvn deploy
 ----
diff --git a/Documentation/error-prohibited-by-gerrit.txt b/Documentation/error-prohibited-by-gerrit.txt
index 3d9bbad..3e5f23b 100644
--- a/Documentation/error-prohibited-by-gerrit.txt
+++ b/Documentation/error-prohibited-by-gerrit.txt
@@ -17,10 +17,10 @@
    link:access-control.html#category_create['Create Reference'] access
    right on `+refs/heads/*+`
 4. if you push an annotated tag without
-   link:access-control.html#category_push_annotated['Push Annotated Tag']
+   link:access-control.html#category_create_annotated['Create Annotated Tag']
    access right on `+refs/tags/*+`
 5. if you push a signed tag without
-   link:access-control.html#category_push_signed['Push Signed Tag']
+   link:access-control.html#category_create_signed['Create Signed Tag']
    access right on `+refs/tags/*+`
 6. if you push a lightweight tag without the access right link:access-control.html#category_create['Create
    Reference'] for the reference name `+refs/tags/*+`
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py
index bc2d657..15f470c 100755
--- a/Documentation/gen_licenses.py
+++ b/Documentation/gen_licenses.py
@@ -95,16 +95,14 @@
 
 if args.asciidoc:
   print("""\
-Gerrit Code Review - Licenses
-=============================
+= Gerrit Code Review - Licenses
 
 Gerrit open source software is licensed under the <<Apache2_0,Apache
 License 2.0>>.  Executable distributions also include other software
 components that are provided under additional licenses.
 
 [[cryptography]]
-Cryptography Notice
--------------------
+== Cryptography Notice
 
 This distribution includes cryptographic software.  The country
 in which you currently reside may have restrictions on the import,
@@ -139,8 +137,7 @@
 link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
 to be installed by the end-user.
 
-Licenses
---------
+== Licenses
 """)
 
 for n in used:
@@ -149,13 +146,13 @@
   if args.asciidoc:
     print()
     print('[[%s]]' % name.replace('.', '_'))
-    print(name)
-    print('~' * len(name))
+    print("=== " + name)
     print()
   else:
     print()
     print(name)
-    print('--')
+    print()
+    print('----')
   for d in libs:
     if d.startswith('//lib:') or d.startswith('//lib/'):
       p = d[len('//lib:'):]
@@ -166,12 +163,12 @@
     print('* ' + p)
   if args.asciidoc:
     print()
-    print('[[license]]')
-    print('[verse]')
-    print('--')
+    print('[[%s_license]]' % name.replace('.', '_'))
+    print('----')
   with open(n[2:].replace(':', '/')) as fd:
     copyfileobj(fd, stdout)
-  print('--')
+  print()
+  print('----')
 
 if args.asciidoc:
   print("""
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 9bf6842..c0af651 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -470,11 +470,16 @@
 link:user-review-ui.html#project-branch-topic[change screen].
 
 It is also possible to link:user-upload.html#topic[set a topic on
-push].
+push], either by appending `%topic=...` to the ref name or through
+the use of the command line flag `--push-option`, aliased to `-o`,
+followed by `topic=...`.
 
 .Set Topic on Push
 ----
   $ git push origin HEAD:refs/for/master%topic=multi-master
+
+  // this is the same as:
+  $ git push origin HEAD:refs/heads/master -o topic=multi-master
 ----
 
 [[drafts]]
@@ -639,6 +644,23 @@
 +
 Email notifications are disabled.
 
+- [[default-base-for-merges]]`Default Base For Merges`:
++
+This setting controls which base should be pre-selected in the
+`Diff Against` drop-down list when the change screen is opened for a
+merge commit.
++
+** `Auto Merge`:
++
+Pre-selects `Auto Merge` in the `Diff Against` drop-down list when the
+change screen is opened for a merge commit.
++
+** `First Parent`:
++
+Pre-selects `Parent 1` in the `Diff Against` drop-down list when the
+change screen is opened for a merge commit.
++
+
 - [[diff-view]]`Diff View`:
 +
 Whether the Side-by-Side diff view or the Unified diff view should be
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
index ee3e8ce..4531446 100644
--- a/Documentation/rest-api-access.txt
+++ b/Documentation/rest-api-access.txt
@@ -132,7 +132,7 @@
         },
         "refs/tags/*": {
           "permissions": {
-            "pushSignedTag": {
+            "createSignedTag": {
               "rules": {
                 "53a4f647a89ea57992571187d8025f830625192a": {
                   "action": "ALLOW"
@@ -142,7 +142,7 @@
                 }
               }
             },
-            "pushTag": {
+            "createTag": {
               "rules": {
                 "53a4f647a89ea57992571187d8025f830625192a": {
                   "action": "ALLOW"
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 2b44855..13e5ca6 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -396,7 +396,7 @@
   HTTP/1.1 204 No Content
 ----
 
-If the account was already inactive the response is "`404 Not Found`".
+If the account was already inactive the response is "`409 Conflict`".
 
 [[get-http-password]]
 === Get HTTP Password
@@ -1213,6 +1213,7 @@
     "size_bar_in_change_table": true,
     "review_category_strategy": "ABBREV",
     "mute_common_path_prefixes": true,
+    "default_base_for_merges": "FIRST_PARENT",
     "my": [
       {
         "url": "#/dashboard/self",
@@ -2469,6 +2470,10 @@
 their own comments. On `DISABLED` the user will not receive any email
 notifications from Gerrit.
 Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`.
+|`default_base_for_merges`      ||
+The base which should be pre-selected in the 'Diff Against' drop-down
+list when the change screen is opened for a merge commit.
+Allowed values are `AUTO_MERGE` and `FIRST_PARENT`.
 |============================================
 
 [[preferences-input]]
@@ -2527,6 +2532,10 @@
 their own comments. On `DISABLED` the user will not receive any email
 notifications from Gerrit.
 Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`.
+|`default_base_for_merges`      |optional|
+The base which should be pre-selected in the 'Diff Against' drop-down
+list when the change screen is opened for a merge commit.
+Allowed values are `AUTO_MERGE` and `FIRST_PARENT`.
 |============================================
 
 [[query-limit-info]]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index be03b79..63eccfb 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2135,9 +2135,17 @@
 
 Promotes change edit to a regular patch set.
 
+Options can be provided in the request body as a
+link:#publish-change-edit-input[PublishChangeEditInput] entity.
+
 .Request
 ----
   POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:publish HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "notify": "NONE"
+  }
 ----
 
 As response "`204 No Content`" is returned.
@@ -2408,14 +2416,33 @@
 [[delete-reviewer]]
 === Delete Reviewer
 --
-'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]'
+'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]' +
+'POST /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/delete'
 --
 
 Deletes a reviewer from a change.
 
+Options can be provided in the request body as a
+link:#delete-reviewer-input[DeleteReviewerInput] entity.
+
 .Request
 ----
   DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe HTTP/1.0
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/delete HTTP/1.0
+----
+
+Please note that some proxies prohibit request bodies for DELETE
+requests. In this case, if you want to specify options, use a POST
+request:
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/delete HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "notify": "NONE"
+  }
 ----
 
 .Response
@@ -2456,7 +2483,7 @@
 [[delete-vote]]
 === Delete Vote
 --
-'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]'
+'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]' +
 'POST /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]/delete'
 --
 
@@ -2545,6 +2572,60 @@
 Adding query parameter `links` (for example `/changes/.../commit?links`)
 returns a link:#commit-info[CommitInfo] with the additional field `web_links`.
 
+[[get-merge-list]]
+=== Get Merge List
+--
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/mergelist'
+--
+
+Returns the list of commits that are being integrated into a target
+branch by a merge commit. By default the first parent is assumed to be
+uninteresting. By using the `parent` option another parent can be set
+as uninteresting (parents are 1-based).
+
+The list of commits is returned as a list of
+link:#commit-info[CommitInfo] entities. Web links are only included if
+the `links` option was set.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/7e30d802b890ec8d0be45b1cc2a8ef092bcfc858/mergelist HTTP/1.0
+----
+
+.Response
+----
+HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  [
+    {
+      "commit": "674ac754f91e64a0efb8087e59a176484bd534d1",
+      "parents": [
+        {
+          "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
+          "subject": "Migrate contributor agreements to All-Projects."
+        }
+      ],
+      "author": {
+        "name": "Shawn O. Pearce",
+        "email": "sop@google.com",
+        "date": "2012-04-24 18:08:08.000000000",
+        "tz": -420
+      },
+      "committer": {
+        "name": "Shawn O. Pearce",
+        "email": "sop@google.com",
+        "date": "2012-04-24 18:08:08.000000000",
+        "tz": -420
+      },
+      "subject": "Use an EventBus to manage star icons",
+      "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+    }
+  ]
+----
+
 [[get-revision-actions]]
 === Get Revision Actions
 --
@@ -4588,6 +4669,21 @@
 link:#web-link-info[WebLinkInfo] entities.
 |===========================
 
+[[delete-reviewer-input]]
+=== DeleteReviewerInput
+The `DeleteReviewerInput` entity contains options for the deletion of a
+reviewer.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name||Description
+|`notify`  |optional|
+Notify handling that defines to whom email notifications should be sent
+after the reviewer is deleted. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
+|=======================
+
 [[delete-vote-input]]
 === DeleteVoteInput
 The `DeleteVoteInput` entity contains options for the deletion of a
@@ -4973,6 +5069,21 @@
 outcome of the fix.
 |===========================
 
+[[publish-change-edit-input]]
+=== PublishChangeEditInput
+The `PublishChangeEditInput` entity contains options for the publishing of
+change edit.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name||Description
+|`notify`  |optional|
+Notify handling that defines to whom email notifications should be sent
+after the change edit is published. +
+Allowed values are `NONE` and `ALL`. +
+If not set, the default is `ALL`.
+|=======================
+
 [[push-certificate-info]]
 === PushCertificateInfo
 The `PushCertificateInfo` entity contains information about a push
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index c7c0878..454a30d 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -54,6 +54,14 @@
   {
     "auth": {
       "auth_type": "LDAP",
+      "use_contributor_agreements": true,
+      "contributor_agreements": [
+        {
+          "name": "Individual",
+          "description": "If you are going to be contributing code on your own, this is the one you want. You can sign this one online.",
+          "url": "static/cla_individual.html"
+        }
+      ],
       "editable_account_fields": [
         "FULL_NAME",
         "REGISTER_NEW_EMAIL"
@@ -1226,6 +1234,9 @@
 |`use_contributor_agreements` |not set if `false`|
 Whether link:config-gerrit.html#auth.contributorAgreements[contributor
 agreements] are required.
+|`contributor_agreements`     |not set if `use_contributor_agreements` is `false`|
+List of contributor agreements as link:rest-api-accounts.html#contributor-agreement-info[
+ContributorAgreementInfo] entities.
 |`editable_account_fields`    ||
 List of account fields that are editable. Possible values are
 `FULL_NAME`, `USER_NAME` and `REGISTER_NEW_EMAIL`.
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index ca79b93..7942059 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -176,12 +176,16 @@
 
 To include a short tag associated with all of the changes in the
 same group, such as the local topic branch name, append it after
-the destination branch name. In this example the short topic tag
-'driver/i42' will be saved on each change this push creates or
+the destination branch name or add it with the command line flag
+`--push-option`, aliased to `-o`. In this example the short topic
+tag 'driver/i42' will be saved on each change this push creates or
 updates:
 
 ----
   git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%topic=driver/i42
+
+  // this is the same as:
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental -o topic=driver/i42
 ----
 
 [[message]]
@@ -399,11 +403,11 @@
 link:access-control.html#category_push_direct['Push'] with the
 'Force' option ticked.
 
-To push annotated tags, the `Push Annotated Tag` project right must
+To push annotated tags, the `Create Annotated Tag` project right must
 be granted to one (or more) of the user's groups.  There is only
 one level of access in this category.
 
-Project owners may wish to grant themselves `Push Annotated Tag`
+Project owners may wish to grant themselves `Create Annotated Tag`
 only at times when a new release is being prepared, and otherwise
 grant nothing at all.  This ensures that accidental pushes don't
 make undesired changes to the public repository.
diff --git a/VERSION b/VERSION
index 573f909..3035c93 100644
--- a/VERSION
+++ b/VERSION
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = '2.13-SNAPSHOT'
+GERRIT_VERSION = '2.14-SNAPSHOT'
diff --git a/WORKSPACE b/WORKSPACE
index d465b37..97fdd05 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -24,24 +24,30 @@
   sha1 = '83cd2cd674a217ade95a4bb83a8a14f351f48bd0',
 )
 
-GUICE_VERS = '4.0'
+GUICE_VERS = '4.1.0'
 
 maven_jar(
   name = 'guice_library',
   artifact = 'com.google.inject:guice:' + GUICE_VERS,
-  sha1 = '0f990a43d3725781b6db7cd0acf0a8b62dfd1649',
+  sha1 = 'eeb69005da379a10071aa4948c48d89250febb07',
 )
 
 maven_jar(
   name = 'guice_assistedinject',
   artifact = 'com.google.inject.extensions:guice-assistedinject:' + GUICE_VERS,
-  sha1 = '8fa6431da1a2187817e3e52e967535899e2e46ca',
+  sha1 = 'af799dd7e23e6fe8c988da12314582072b07edcb',
 )
 
 maven_jar(
   name = 'guice_servlet',
   artifact = 'com.google.inject.extensions:guice-servlet:' + GUICE_VERS,
-  sha1 = '4503da866f4c402b5090579b40c1c4aaefabb164',
+  sha1 = '90ac2db772d9b85e2b05417b74f7464bcc061dcb',
+)
+
+maven_jar(
+  name = 'multibindings',
+  artifact = 'com.google.inject.extensions:guice-multibindings:' + GUICE_VERS,
+  sha1 = '3b27257997ac51b0f8d19676f1ea170427e86d51',
 )
 
 maven_jar(
@@ -82,27 +88,27 @@
   sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e',
 )
 
-JGIT_VERS = '4.4.1.201607150455-r.105-g81ba2be'
+JGIT_VERS = '4.4.1.201607150455-r.118-g1096652'
 
 maven_jar(
   name = 'jgit',
   repository = 'http://gerrit-maven.storage.googleapis.com/',
   artifact = 'org.eclipse.jgit:org.eclipse.jgit:' + JGIT_VERS,
-  sha1 = 'c07c9c66da7983095a40945c0bfab211a473c4c5',
+  sha1 = 'cd142b9030910babd119702f1c4eeae13ee90018',
 )
 
 maven_jar(
   name = 'jgit_servlet',
   repository = 'http://gerrit-maven.storage.googleapis.com/',
   artifact = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + JGIT_VERS,
-  sha1 = 'bb01841b74a48abe506c2e44f238e107188e6c8f',
+  sha1 = 'fa67bf925001cfc663bf98772f37d5c5c1abd756',
 )
 
 # TODO(davido): Remove this hack when maven_jar supports pulling sources
 # https://github.com/bazelbuild/bazel/issues/308
 http_file(
   name = 'jgit_src',
-  sha256 = '881906cb1e6743cb78df6dd3788cab7e974308fbb98cab4915e6591a62aa9374',
+  sha256 = '1a0b2d637359b1b51eba4d094491ef39877a6fc192e2fc1da0422a9adf04f0b8',
   url = 'http://gerrit-maven.storage.googleapis.com/org/eclipse/jgit/org.eclipse.jgit/' +
       '%s/org.eclipse.jgit-%s-sources.jar' % (JGIT_VERS, JGIT_VERS),
 )
@@ -117,14 +123,14 @@
   name = 'jgit_archive',
   repository = 'http://gerrit-maven.storage.googleapis.com/',
   artifact = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + JGIT_VERS,
-  sha1 = 'fc3bc40e070c54198a046fcd3a1f7cac47163961',
+  sha1 = '3f45cd199e40a7c68ee07a1743c06d1c3d07308a',
 )
 
 maven_jar(
   name = 'jgit_junit',
   repository = 'http://gerrit-maven.storage.googleapis.com/',
   artifact = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + JGIT_VERS,
-  sha1 = 'b4565ee84a6e1d0952010282b9fcf705ac6171a7',
+  sha1 = 'dc7edb9c3060655c7fb93ab9b9349e815bab266f',
 )
 
 maven_jar(
@@ -141,8 +147,8 @@
 
 maven_jar(
   name = 'gson',
-  artifact = 'com.google.code.gson:gson:2.6.2',
-  sha1 = 'f1bc476cc167b18e66c297df599b2377131a8947',
+  artifact = 'com.google.code.gson:gson:2.7',
+  sha1 = '751f548c85fa49f330cecbb1875893f971b33c4e',
 )
 
 maven_jar(
@@ -165,14 +171,14 @@
 
 maven_jar(
   name = 'joda_time',
-  artifact = 'joda-time:joda-time:2.8',
-  sha1 = '9f2785d7184b97d005a44241ccaf980f43b9ccdb',
+  artifact = 'joda-time:joda-time:2.9.4',
+  sha1 = '1c295b462f16702ebe720bbb08f62e1ba80da41b',
 )
 
 maven_jar(
   name = 'joda_convert',
-  artifact = 'org.joda:joda-convert:1.2',
-  sha1 = '35ec554f0cd00c956cc69051514d9488b1374dec',
+  artifact = 'org.joda:joda-convert:1.8.1',
+  sha1 = '675642ac208e0b741bc9118dcbcae44c271b992a',
 )
 
 maven_jar(
@@ -287,8 +293,8 @@
 
 maven_jar(
   name = 'commons_net',
-  artifact = 'commons-net:commons-net:2.2',
-  sha1 = '07993c12f63c78378f8c90de4bc2ee62daa7ca3a',
+  artifact = 'commons-net:commons-net:3.5',
+  sha1 = '342fc284019f590e1308056990fdb24a08f06318',
 )
 
 maven_jar(
@@ -327,42 +333,42 @@
   sha1 = '2e35862b0435c1b027a21f3d6eecbe50e6e08d54',
 )
 
-OW2_VERS = '5.0.3'
+OW2_VERS = '5.1'
 
 maven_jar(
   name = 'ow2_asm',
   artifact = 'org.ow2.asm:asm:' + OW2_VERS,
-  sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa',
+  sha1 = '5ef31c4fe953b1fd00b8a88fa1d6820e8785bb45',
 )
 
 maven_jar(
   name = 'ow2_asm_analysis',
   artifact = 'org.ow2.asm:asm-analysis:' + OW2_VERS,
-  sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3',
+  sha1 = '6d1bf8989fc7901f868bee3863c44f21aa63d110',
 )
 
 maven_jar(
   name = 'ow2_asm_commons',
   artifact = 'org.ow2.asm:asm-commons:' + OW2_VERS,
-  sha1 = 'a7111830132c7f87d08fe48cb0ca07630f8cb91c',
+  sha1 = '25d8a575034dd9cfcb375a39b5334f0ba9c8474e',
 )
 
 maven_jar(
   name = 'ow2_asm_tree',
   artifact = 'org.ow2.asm:asm-tree:' + OW2_VERS,
-  sha1 = '287749b48ba7162fb67c93a026d690b29f410bed',
+  sha1 = '87b38c12a0ea645791ead9d3e74ae5268d1d6c34',
 )
 
 maven_jar(
   name = 'ow2_asm_util',
   artifact = 'org.ow2.asm:asm-util:' + OW2_VERS,
-  sha1 = '1512e5571325854b05fb1efce1db75fcced54389',
+  sha1 = 'b60e33a6bd0d71831e0c249816d01e6c1dd90a47',
 )
 
 maven_jar(
   name = 'auto_value',
-  artifact = 'com.google.auto.value:auto-value:1.2',
-  sha1 = '6873fed014fe1de1051aae2af68ba266d2934471',
+  artifact = 'com.google.auto.value:auto-value:1.3-rc1',
+  sha1 = 'b764e0fb7e11353fbff493b22fd6e83bf091a179',
 )
 
 maven_jar(
@@ -371,36 +377,36 @@
   sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3',
 )
 
-LUCENE_VERS = '5.4.1'
+LUCENE_VERS = '5.5.0'
 
 maven_jar(
   name = 'lucene_core',
   artifact = 'org.apache.lucene:lucene-core:' + LUCENE_VERS,
-  sha1 = 'c52b2088e2c30dfd95fd296ab6fb9cf8de9855ab',
+  sha1 = 'a74fd869bb5ad7fe6b4cd29df9543a34aea81164',
 )
 
 maven_jar(
   name = 'lucene_analyzers_common',
   artifact = 'org.apache.lucene:lucene-analyzers-common:' + LUCENE_VERS,
-  sha1 = 'c2aa2c4e00eb9cdeb5ac00dc0495e70c441f681e',
+  sha1 = '1e0e8243a4410be20c34683034fafa7bb52e55cc',
 )
 
 maven_jar(
   name = 'backward_codecs',
   artifact = 'org.apache.lucene:lucene-backward-codecs:' + LUCENE_VERS,
-  sha1 = '5273da96380dfab302ad06c27fe58100db4c4e2f',
+  sha1 = '68480974b2f54f519763632a7c1c5d51cbff3805',
 )
 
 maven_jar(
   name = 'lucene_misc',
   artifact = 'org.apache.lucene:lucene-misc:' + LUCENE_VERS,
-  sha1 = '95f433b9d7dd470cc0aa5076e0f233907745674b',
+  sha1 = '504d855a1a38190622fdf990b2298c067e7d60ca',
 )
 
 maven_jar(
   name = 'lucene_queryparser',
   artifact = 'org.apache.lucene:lucene-queryparser:' + LUCENE_VERS,
-  sha1 = 'dccd5279bfa656dec21af444a7a66820eb1cd618',
+  sha1 = '0fddc49725b562fd48dff0cff004336ad2a090a4',
 )
 
 maven_jar(
@@ -447,8 +453,8 @@
 
 maven_jar(
   name = 'jsr305',
-  artifact = 'com.google.code.findbugs:jsr305:2.0.2',
-  sha1 = '516c03b21d50a644d538de0f0369c620989cd8f0',
+  artifact = 'com.google.code.findbugs:jsr305:3.0.1',
+  sha1 = 'f7be08ec23c21485b9b5a1cf1654c2ec8c58168d',
 )
 
 maven_jar(
@@ -458,6 +464,19 @@
   sha1 = '51d35e6f8bbc2412265066cea9653dd758c95826',
 )
 
+# Keep this version of Soy synchronized with the version used in Gitiles.
+maven_jar(
+  name = 'soy',
+  artifact = 'com.google.template:soy:2016-08-09',
+  sha1 = '43d33651e95480d515fe26c10a662faafe3ad1e4',
+)
+
+maven_jar(
+  name = 'icu4j',
+  artifact = 'com.ibm.icu:icu4j:57.1',
+  sha1 = '198ea005f41219f038f4291f0b0e9f3259730e92',
+)
+
 maven_jar(
   name = 'dropwizard_core',
   artifact = 'io.dropwizard.metrics:metrics-core:3.1.2',
diff --git a/contrib/build-consistency.go b/contrib/build-consistency.go
new file mode 100644
index 0000000..db63a27
--- /dev/null
+++ b/contrib/build-consistency.go
@@ -0,0 +1,108 @@
+// 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 main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+)
+
+var (
+	// Define regex to find a comment in the build files
+	commentRE = regexp.MustCompile("#.*")
+	// Define regexes to extract the lib name and sha1
+	mvnRE     = regexp.MustCompile("maven_jar([^)]*)")
+	sha1RE    = regexp.MustCompile("sha1=[\"'](?P<SHA1>[^,]*)[\"']")
+	bSha1RE   = regexp.MustCompile("bin_sha1=[\"'](?P<SHA1>[^,]*)[\"']")
+	libNameRE = regexp.MustCompile("name=[\"'](?P<NAME>[^,]*)[\"']")
+)
+
+func sanitize(s string) string {
+	// Strip out comments
+	s = commentRE.ReplaceAllString(s, "")
+	// Remove newlines and blanks
+	s = strings.Replace(s, "\n", "", -1)
+	s = strings.Replace(s, " ", "", -1)
+	// WORKSPACE syntax disallows the dash char in artifact name and we use an underscore
+	// So we make this a consistent underscore in all files
+	s = strings.Replace(s, "-", "_", -1)
+	return s
+}
+
+func main() {
+	// Load bazel WORKSPACE file
+	bzlDat, err := ioutil.ReadFile("WORKSPACE")
+	if err != nil {
+		log.Fatal(err)
+	}
+	bzlStr := sanitize(string(bzlDat))
+
+	// Walk all files nested under lib. Find, load and sanitize BUCK files
+	bckStrs := []string{}
+	err = filepath.Walk("lib/", func(path string, f os.FileInfo, err error) error {
+		bckFile := filepath.Join(path, "BUCK")
+		if _, err := os.Stat(bckFile); err == nil {
+			bckDat, err := ioutil.ReadFile(bckFile)
+			if err != nil {
+				return err
+			}
+			bckStrs = append(bckStrs, sanitize(string(bckDat)))
+		}
+		return nil
+	})
+	if err != nil {
+		log.Fatal(err)
+	}
+	bckStr := strings.Join(bckStrs, "")
+
+	// Find all bazel dependencies
+	// bzlVersions maps from a lib name to the referenced sha1
+	bzlVersions := make(map[string]string)
+	for _, mvn := range mvnRE.FindAllString(bzlStr, -1) {
+		sha1s := sha1RE.FindStringSubmatch(mvn)
+		names := libNameRE.FindStringSubmatch(mvn)
+		if len(sha1s) > 1 && len(names) > 1 {
+			bzlVersions[names[1]] = sha1RE.FindStringSubmatch(mvn)[1]
+		} else {
+			fmt.Printf("Can't parse lib sha1/name of target %s\n", mvn)
+		}
+	}
+
+	// Find all buck dependencies and check if we have the correct bazel dependency on file
+	for _, mvn := range mvnRE.FindAllString(bckStr, -1) {
+		sha1s := bSha1RE.FindStringSubmatch(mvn)
+		if len(sha1s) < 2 {
+			// Buck knows two dep version representations: just a SHA1 or a bin_sha1 and src_sha1
+			// We try to extract the bin_sha1 first. If that fails, we use the sha1
+			sha1s = sha1RE.FindStringSubmatch(mvn)
+		}
+		names := libNameRE.FindStringSubmatch(mvn)
+		if len(sha1s) > 1 && len(names) > 1 {
+			if _, ok := bzlVersions[names[1]]; !ok {
+				// TODO(hiesel) This produces too many false positives.
+				//fmt.Printf("Don't have lib %s in bazel\n", names[1])
+			} else if bzlVersions[names[1]] != sha1s[1] {
+				fmt.Printf("SHA1 of lib %s does not match: buck has %s while bazel has %s\n", names[1], sha1s[1], bzlVersions[names[1]])
+			}
+		} else {
+			fmt.Printf("Can't parse lib sha1/name on target %s\n", mvn)
+		}
+	}
+}
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK
index ba68fa3..4bce902 100644
--- a/gerrit-acceptance-framework/BUCK
+++ b/gerrit-acceptance-framework/BUCK
@@ -1,6 +1,7 @@
 SRCS = glob(['src/test/java/com/google/gerrit/acceptance/*.java'])
 
 DEPS = [
+  '//gerrit-antlr:query_exception',
   '//gerrit-gpg:gpg',
   '//gerrit-launcher:launcher',
   '//gerrit-openid:openid',
@@ -37,10 +38,20 @@
 
 java_binary(
   name = 'acceptance-framework',
+  merge_manifests = False,
+  manifest_file = ':manifest',
   deps = [':lib'],
   visibility = ['PUBLIC'],
 )
 
+genrule(
+  name = 'manifest',
+  cmd = 'echo "Manifest-Version: 1.0" >$OUT;' +
+    'echo "Implementation-Title: Gerrit Acceptance Test Framework" >>$OUT;' +
+    'echo "Implementation-Vendor: Gerrit Code Review Project" >>$OUT',
+  out = 'manifest.txt',
+)
+
 java_library(
   name = 'lib',
   srcs = SRCS,
@@ -57,18 +68,8 @@
 )
 
 java_sources(
-  name = 'src',
-  srcs = SRCS,
-  visibility = ['PUBLIC'],
-)
-
-# The above java_sources produces a .jar somewhere in the depths of
-# buck-out, but it does not bring it to
-# buck-out/gen/gerrit-acceptance-framework/gerrit-acceptance-framework-src.jar.
-# We fix that by the following java_binary.
-java_binary(
   name = 'acceptance-framework-src',
-  deps = [ ':src' ],
+  srcs = SRCS,
   visibility = ['PUBLIC'],
 )
 
@@ -76,7 +77,7 @@
   name = 'acceptance-framework-javadoc',
   title = 'Gerrit Acceptance Test Framework Documentation',
   pkgs = [' com.google.gerrit.acceptance'],
-  paths = ['src/test/java'],
+  source_jar = ':acceptance-framework-src',
   srcs = SRCS,
   deps = DEPS + PROVIDED + [
     '//lib:guava',
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD
index 1439ba9..934e8d1 100644
--- a/gerrit-acceptance-framework/BUILD
+++ b/gerrit-acceptance-framework/BUILD
@@ -3,6 +3,7 @@
 SRCS = glob(['src/test/java/com/google/gerrit/acceptance/*.java'])
 
 DEPS = [
+  '//gerrit-antlr:query_exception',
   '//gerrit-gpg:gpg',
   '//gerrit-launcher:launcher',
   '//gerrit-openid:openid',
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index b5156c0..d9d701c 100644
--- a/gerrit-acceptance-framework/pom.xml
+++ b/gerrit-acceptance-framework/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
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 ae480c7..db4658a 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
@@ -30,12 +30,15 @@
 import com.google.common.primitives.Chars;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 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.changes.SubmittedTogetherInfo;
+import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.groups.GroupInput;
 import com.google.gerrit.extensions.api.projects.BranchApi;
 import com.google.gerrit.extensions.api.projects.BranchInput;
@@ -61,6 +64,7 @@
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.change.Abandon;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Revisions;
@@ -70,6 +74,8 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.index.change.ChangeIndexCollection;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.mail.EmailHeader;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
@@ -217,6 +223,9 @@
   @Inject
   private EventRecorder.Factory eventRecorderFactory;
 
+  @Inject
+  private ChangeIndexCollection changeIndexes;
+
   protected TestRepository<InMemoryRepository> testRepo;
   protected GerritServer server;
   protected TestAccount admin;
@@ -235,6 +244,9 @@
   @Inject
   protected ChangeNotes.Factory notesFactory;
 
+  @Inject
+  protected Abandon changeAbandoner;
+
   @Rule
   public ExpectedException exception = ExpectedException.none();
 
@@ -666,6 +678,22 @@
     atrScope.set(preDisableContext);
   }
 
+  protected void disableChangeIndexWrites() {
+    for (ChangeIndex i : changeIndexes.getWriteIndexes()) {
+      if (!(i instanceof ReadOnlyChangeIndex)) {
+        changeIndexes.addWriteIndex(new ReadOnlyChangeIndex(i));
+      }
+    }
+  }
+
+  protected void enableChangeIndexWrites() {
+    for (ChangeIndex i : changeIndexes.getWriteIndexes()) {
+      if (i instanceof ReadOnlyChangeIndex) {
+        changeIndexes.addWriteIndex(((ReadOnlyChangeIndex)i).unwrap());
+      }
+    }
+  }
+
   protected static Gson newGson() {
     return OutputFormat.JSON_COMPACT.newGson();
   }
@@ -800,6 +828,19 @@
     }
   }
 
+  protected void removePermission(String permission, Project.NameKey project,
+      String ref) throws IOException, ConfigInvalidException {
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      md.setMessage(String.format("Remove %s on %s", permission, ref));
+      ProjectConfig config = ProjectConfig.read(md);
+      AccessSection s = config.getAccessSection(ref, true);
+      Permission p = s.getPermission(permission, true);
+      p.getRules().clear();
+      config.commit(md);
+      projectCache.evict(config.getProject());
+    }
+  }
+
   protected void blockRead(String ref) throws Exception {
     block(Permission.READ, REGISTERED_USERS, ref);
   }
@@ -940,4 +981,31 @@
         (EmailHeader.String)message.headers().get("Reply-To");
     assertThat(replyTo.getString()).isEqualTo(email);
   }
+
+  protected ContributorAgreement configureContributorAgreement(
+      boolean autoVerify) throws Exception {
+    ContributorAgreement ca;
+    if (autoVerify) {
+      String g = createGroup("cla-test-group");
+      GroupApi groupApi = gApi.groups().id(g);
+      groupApi.description("CLA test group");
+      AccountGroup caGroup = groupCache.get(
+          new AccountGroup.UUID(groupApi.detail().id));
+      GroupReference groupRef = GroupReference.forGroup(caGroup);
+      PermissionRule rule = new PermissionRule(groupRef);
+      rule.setAction(PermissionRule.Action.ALLOW);
+      ca = new ContributorAgreement("cla-test");
+      ca.setAutoVerify(groupRef);
+      ca.setAccepted(ImmutableList.of(rule));
+    } else {
+      ca = new ContributorAgreement("cla-test-no-auto-verify");
+    }
+    ca.setDescription("description");
+    ca.setAgreementUrl("agreement-url");
+
+    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+    cfg.replace(ca);
+    saveProjectConfig(allProjects, cfg);
+    return ca;
+  }
 }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
index 0196d1f..ceab04fe 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
@@ -28,12 +28,15 @@
 
 import org.eclipse.jgit.api.FetchCommand;
 import org.eclipse.jgit.api.PushCommand;
+import org.eclipse.jgit.api.TagCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.FetchResult;
@@ -137,6 +140,26 @@
     return cloneProject(project, sshSession.getUrl() + "/" + project.get());
   }
 
+  public static Ref createAnnotatedTag(TestRepository<?> testRepo, String name,
+      PersonIdent tagger) throws GitAPIException {
+    TagCommand cmd = testRepo.git().tag()
+        .setName(name)
+        .setAnnotated(true)
+        .setMessage(name)
+        .setTagger(tagger);
+    return cmd.call();
+  }
+
+  public static Ref updateAnnotatedTag(TestRepository<?> testRepo, String name,
+      PersonIdent tagger) throws GitAPIException {
+    TagCommand tc = testRepo.git().tag().setName(name);
+    return tc.setAnnotated(true)
+        .setMessage(name)
+        .setTagger(tagger)
+        .setForceUpdate(true)
+        .call();
+  }
+
   public static void fetch(TestRepository<?> testRepo, String spec)
       throws GitAPIException {
     FetchCommand fetch = testRepo.git().fetch();
@@ -144,6 +167,11 @@
     fetch.call();
   }
 
+  public static PushResult pushHead(TestRepository<?> testRepo, String ref)
+      throws GitAPIException {
+    return pushHead(testRepo, ref, false);
+  }
+
   public static PushResult pushHead(TestRepository<?> testRepo, String ref,
       boolean pushTags) throws GitAPIException {
     return pushHead(testRepo, ref, pushTags, false);
@@ -151,9 +179,27 @@
 
   public static PushResult pushHead(TestRepository<?> testRepo, String ref,
       boolean pushTags, boolean force) throws GitAPIException {
+    return pushOne(testRepo, "HEAD", ref, pushTags, force, null);
+  }
+
+  public static PushResult pushHead(TestRepository<?> testRepo, String ref,
+      boolean pushTags, boolean force, List<String> pushOptions)
+          throws GitAPIException {
+    return pushOne(testRepo, "HEAD", ref, pushTags, force, pushOptions);
+  }
+
+  public static PushResult deleteRef(TestRepository<?> testRepo, String ref)
+      throws GitAPIException {
+    return pushOne(testRepo, "", ref, false, true, null);
+  }
+
+  public static PushResult pushOne(TestRepository<?> testRepo, String source,
+      String target, boolean pushTags, boolean force, List<String> pushOptions)
+          throws GitAPIException {
     PushCommand pushCmd = testRepo.git().push();
     pushCmd.setForce(force);
-    pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref));
+    pushCmd.setPushOptions(pushOptions);
+    pushCmd.setRefSpecs(new RefSpec(source + ":" + target));
     if (pushTags) {
       pushCmd.setPushTags();
     }
@@ -175,6 +221,20 @@
     assertThat(rru.getMessage()).isEqualTo(expectedMessage);
   }
 
+  public static PushResult pushTag(TestRepository<?> testRepo, String tag)
+      throws GitAPIException {
+    return pushTag(testRepo, tag, false);
+  }
+
+  public static PushResult pushTag(TestRepository<?> testRepo, String tag,
+      boolean force) throws GitAPIException {
+    PushCommand pushCmd = testRepo.git().push();
+    pushCmd.setForce(force);
+    pushCmd.setRefSpecs(new RefSpec("refs/tags/" + tag + ":refs/tags/" + tag));
+    Iterable<PushResult> r = pushCmd.call();
+    return Iterables.getOnlyElement(r);
+  }
+
   public static Optional<String> getChangeId(TestRepository<?> tr, ObjectId id)
       throws IOException {
     RevCommit c = tr.getRevWalk().parseCommit(id);
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
index 390cae3..e9c6e96 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Preconditions;
 
+import org.apache.http.Header;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
 
@@ -52,7 +53,12 @@
   }
 
   public String getContentType() {
-    return response.getFirstHeader("X-FYI-Content-Type").getValue();
+    return getHeader("X-FYI-Content-Type");
+  }
+
+  public String getHeader(String name) {
+    Header hdr = response.getFirstHeader(name);
+    return hdr != null ? hdr.getValue() : null;
   }
 
   public boolean hasContent() {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
index 1e0920e..669b991 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
@@ -37,7 +37,11 @@
             account.username, account.httpPassword);
   }
 
-  protected RestResponse execute(Request request) throws IOException {
+  public String url() {
+    return url;
+  }
+
+  public RestResponse execute(Request request) throws IOException {
     return new RestResponse(executor.execute(request).returnResponse());
   }
 }
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 d79e573..5505263 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
@@ -16,6 +16,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static org.junit.Assert.assertEquals;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
@@ -136,6 +137,7 @@
   private String changeId;
   private Tag tag;
   private boolean force;
+  private List<String> pushOptions;
 
   private final TestRepository<?>.CommitBuilder commitBuilder;
 
@@ -275,8 +277,8 @@
       }
       tagCommand.call();
     }
-    return new Result(ref, pushHead(testRepo, ref, tag != null, force), c,
-        subject);
+    return new Result(ref,
+        pushHead(testRepo, ref, tag != null, force, pushOptions), c, subject);
   }
 
   public void setTag(final Tag tag) {
@@ -287,6 +289,14 @@
     this.force = force;
   }
 
+  public List<String> getPushOptions() {
+    return pushOptions;
+  }
+
+  public void setPushOptions(List<String> pushOptions) {
+    this.pushOptions = pushOptions;
+  }
+
   public void noParents() {
     commitBuilder.noParents();
   }
@@ -326,6 +336,10 @@
       return commit;
     }
 
+    public void assertPushOptions(List<String> pushOptions) {
+      assertEquals(pushOptions, getPushOptions());
+    }
+
     public void assertChange(Change.Status expectedStatus,
         String expectedTopic, TestAccount... expectedReviewers)
         throws OrmException, NoSuchChangeException {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
new file mode 100644
index 0000000..cdecf05
--- /dev/null
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.server.index.QueryOptions;
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.query.DataSource;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+
+import java.io.IOException;
+
+public class ReadOnlyChangeIndex implements ChangeIndex {
+  private final ChangeIndex index;
+
+  public ReadOnlyChangeIndex(ChangeIndex index) {
+    this.index = index;
+  }
+
+  public ChangeIndex unwrap() {
+    return index;
+  }
+
+  @Override
+  public Schema<ChangeData> getSchema() {
+    return index.getSchema();
+  }
+
+  @Override
+  public void close() {
+    index.close();
+  }
+
+  @Override
+  public void replace(ChangeData obj) throws IOException {
+    // do nothing
+  }
+
+  @Override
+  public void delete(Id key) throws IOException {
+    // do nothing
+  }
+
+  @Override
+  public void deleteAll() throws IOException {
+    // do nothing
+  }
+
+  @Override
+  public DataSource<ChangeData> getSource(Predicate<ChangeData> p,
+      QueryOptions opts) throws QueryParseException {
+    return index.getSource(p, opts);
+  }
+
+  @Override
+  public void markReady(boolean ready) throws IOException {
+    // do nothing
+  }
+}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 9c59e10..90ece46 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -45,7 +45,7 @@
         new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
   }
 
-  private RestResponse getWithHeader(String endPoint, Header header)
+  public RestResponse getWithHeader(String endPoint, Header header)
       throws IOException {
     Request get = Request.Get(url + "/a" + endPoint);
     if (header != null) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 93525a4..ec77484 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -29,6 +29,7 @@
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.fail;
 
 import com.google.common.base.Function;
 import com.google.common.collect.FluentIterable;
@@ -199,6 +200,36 @@
   }
 
   @Test
+  public void active() throws Exception {
+    assertThat(gApi.accounts().id("user").getActive()).isTrue();
+    gApi.accounts().id("user").setActive(false);
+    assertThat(gApi.accounts().id("user").getActive()).isFalse();
+    gApi.accounts().id("user").setActive(true);
+    assertThat(gApi.accounts().id("user").getActive()).isTrue();
+  }
+
+  @Test
+  public void deactivateSelf() throws Exception {
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("cannot deactivate own account");
+    gApi.accounts().self().setActive(false);
+  }
+
+  @Test
+  public void deactivateNotActive() throws Exception {
+    assertThat(gApi.accounts().id("user").getActive()).isTrue();
+    gApi.accounts().id("user").setActive(false);
+    assertThat(gApi.accounts().id("user").getActive()).isFalse();
+    try {
+      gApi.accounts().id("user").setActive(false);
+      fail("Expected exception");
+    } catch (ResourceConflictException e) {
+      assertThat(e.getMessage()).isEqualTo("account not active");
+    }
+    gApi.accounts().id("user").setActive(true);
+  }
+
+  @Test
   public void starUnstarChange() throws Exception {
     PushOneCommit.Result r = createChange();
     String triplet = project.get() + "~master~" + r.getChangeId();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 8cd696c..00b48b4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -19,27 +19,22 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.fail;
 
-import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
-import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.common.AgreementInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.TestTimeUtil;
 
@@ -52,8 +47,8 @@
 import java.util.List;
 
 public class AgreementsIT extends AbstractDaemonTest {
-  private ContributorAgreement ca;
-  private ContributorAgreement ca2;
+  private ContributorAgreement caAutoVerify;
+  private ContributorAgreement caNoAutoVerify;
 
   @ConfigSuite.Config
   public static Config enableAgreementsConfig() {
@@ -74,32 +69,26 @@
 
   @Before
   public void setUp() throws Exception {
-    String g = createGroup("cla-test-group");
-    GroupApi groupApi = gApi.groups().id(g);
-    groupApi.description("CLA test group");
-    AccountGroup caGroup = groupCache.get(
-        new AccountGroup.UUID(groupApi.detail().id));
-    GroupReference groupRef = GroupReference.forGroup(caGroup);
-    PermissionRule rule = new PermissionRule(groupRef);
-    rule.setAction(PermissionRule.Action.ALLOW);
-    ca = new ContributorAgreement("cla-test");
-    ca.setDescription("description");
-    ca.setAgreementUrl("agreement-url");
-    ca.setAutoVerify(groupRef);
-    ca.setAccepted(ImmutableList.of(rule));
-
-    ca2 = new ContributorAgreement("cla-test-no-auto-verify");
-    ca2.setDescription("description");
-    ca2.setAgreementUrl("agreement-url");
-
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    cfg.replace(ca);
-    cfg.replace(ca2);
-    saveProjectConfig(allProjects, cfg);
+    caAutoVerify = configureContributorAgreement(true);
+    caNoAutoVerify = configureContributorAgreement(false);
     setApiUser(user);
   }
 
   @Test
+  public void getAvailableAgreements() throws Exception {
+    ServerInfo info = gApi.config().server().getInfo();
+    if (isContributorAgreementsEnabled()) {
+      assertThat(info.auth.useContributorAgreements).isTrue();
+      assertThat(info.auth.contributorAgreements).hasSize(2);
+      assertAgreement(info.auth.contributorAgreements.get(0), caAutoVerify);
+      assertAgreement(info.auth.contributorAgreements.get(1), caNoAutoVerify);
+    } else {
+      assertThat(info.auth.useContributorAgreements).isNull();
+      assertThat(info.auth.contributorAgreements).isNull();
+    }
+  }
+
+  @Test
   public void signNonExistingAgreement() throws Exception {
     assume().that(isContributorAgreementsEnabled()).isTrue();
     exception.expect(UnprocessableEntityException.class);
@@ -112,7 +101,7 @@
     assume().that(isContributorAgreementsEnabled()).isTrue();
     exception.expect(BadRequestException.class);
     exception.expectMessage("cannot enter a non-autoVerify agreement");
-    gApi.accounts().self().signAgreement(ca2.getName());
+    gApi.accounts().self().signAgreement(caNoAutoVerify.getName());
   }
 
   @Test
@@ -124,7 +113,7 @@
     assertThat(result).isEmpty();
 
     // Sign the agreement
-    gApi.accounts().self().signAgreement(ca.getName());
+    gApi.accounts().self().signAgreement(caAutoVerify.getName());
 
     // Explicitly reset the user to force a new request context
     setApiUser(user);
@@ -133,12 +122,10 @@
     result = gApi.accounts().self().listAgreements();
     assertThat(result).hasSize(1);
     AgreementInfo info = result.get(0);
-    assertThat(info.name).isEqualTo(ca.getName());
-    assertThat(info.description).isEqualTo(ca.getDescription());
-    assertThat(info.url).isEqualTo(ca.getAgreementUrl());
+    assertAgreement(info, caAutoVerify);
 
     // Signing the same agreement again has no effect
-    gApi.accounts().self().signAgreement(ca.getName());
+    gApi.accounts().self().signAgreement(caAutoVerify.getName());
     result = gApi.accounts().self().listAgreements();
     assertThat(result).hasSize(1);
   }
@@ -148,7 +135,7 @@
     assume().that(isContributorAgreementsEnabled()).isFalse();
     exception.expect(MethodNotAllowedException.class);
     exception.expectMessage("contributor agreements disabled");
-    gApi.accounts().self().signAgreement(ca.getName());
+    gApi.accounts().self().signAgreement(caAutoVerify.getName());
   }
 
   @Test
@@ -227,7 +214,7 @@
     }
 
     // Sign the agreement
-    gApi.accounts().self().signAgreement(ca.getName());
+    gApi.accounts().self().signAgreement(caAutoVerify.getName());
 
     // Explicitly reset the user to force a new request context
     setApiUser(user);
@@ -236,6 +223,18 @@
     gApi.changes().create(newChangeInput());
   }
 
+  private void assertAgreement(AgreementInfo info, ContributorAgreement ca) {
+    assertThat(info.name).isEqualTo(ca.getName());
+    assertThat(info.description).isEqualTo(ca.getDescription());
+    assertThat(info.url).isEqualTo(ca.getAgreementUrl());
+    if (ca.getAutoVerify() != null) {
+      assertThat(info.autoVerifyGroup.name)
+          .isEqualTo(ca.getAutoVerify().getName());
+    } else {
+      assertThat(info.autoVerifyGroup).isNull();
+    }
+  }
+
   private ChangeInput newChangeInput() {
     ChangeInput in = new ChangeInput();
     in.branch = "master";
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
index f45bfbbe..f8cfbc9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DefaultBase;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
@@ -87,6 +88,7 @@
     i.dateFormat = DateFormat.US;
     i.timeFormat = TimeFormat.HHMM_24;
     i.emailStrategy = EmailStrategy.DISABLED;
+    i.defaultBaseForMerges = DefaultBase.AUTO_MERGE;
     i.relativeDateInChangeTable ^= true;
     i.sizeBarInChangeTable ^= true;
     i.legacycidInChangeTable ^= true;
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 006d8a4..520c7db 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
@@ -31,6 +31,7 @@
 import static org.junit.Assert.fail;
 
 import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -41,9 +42,11 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
@@ -58,6 +61,7 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
@@ -71,6 +75,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -78,7 +83,6 @@
 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 com.google.gerrit.testutil.TestTimeUtil;
 
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -184,6 +188,64 @@
   }
 
   @Test
+  public void batchAbandon() throws Exception {
+    CurrentUser user = atrScope.get().getUser();
+    PushOneCommit.Result a = createChange();
+    List<ChangeControl> controlA = changeFinder.find(a.getChangeId(), user);
+    assertThat(controlA).hasSize(1);
+    PushOneCommit.Result b = createChange();
+    List<ChangeControl> controlB = changeFinder.find(b.getChangeId(), user);
+    assertThat(controlB).hasSize(1);
+    List<ChangeControl> list =
+        ImmutableList.of(controlA.get(0), controlB.get(0));
+    changeAbandoner.batchAbandon(
+        controlA.get(0).getProject().getNameKey(), user, list, "deadbeef");
+
+    ChangeInfo info = get(a.getChangeId());
+    assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+        .contains("abandoned");
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+        .contains("deadbeef");
+
+    info = get(b.getChangeId());
+    assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+        .contains("abandoned");
+    assertThat(Iterables.getLast(info.messages).message.toLowerCase())
+        .contains("deadbeef");
+  }
+
+  @Test
+  public void batchAbandonChangeProject() throws Exception {
+    String project1Name = name("Project1");
+    String project2Name = name("Project2");
+    gApi.projects().create(project1Name);
+    gApi.projects().create(project2Name);
+    TestRepository<InMemoryRepository> project1 =
+        cloneProject(new Project.NameKey(project1Name));
+    TestRepository<InMemoryRepository> project2 =
+        cloneProject(new Project.NameKey(project2Name));
+
+    CurrentUser user = atrScope.get().getUser();
+    PushOneCommit.Result a =
+        createChange(project1, "master", "x", "x", "x", "");
+    List<ChangeControl> controlA = changeFinder.find(a.getChangeId(), user);
+    assertThat(controlA).hasSize(1);
+    PushOneCommit.Result b =
+        createChange(project2, "master", "x", "x", "x", "");
+    List<ChangeControl> controlB = changeFinder.find(b.getChangeId(), user);
+    assertThat(controlB).hasSize(1);
+    List<ChangeControl> list =
+        ImmutableList.of(controlA.get(0), controlB.get(0));
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage(String.format(
+        "Project name \"%s\" doesn't match \"%s\"",
+        project2Name, project1Name));
+    changeAbandoner.batchAbandon(new Project.NameKey(project1Name), user, list);
+  }
+
+  @Test
   public void abandonDraft() throws Exception {
     PushOneCommit.Result r = createDraftChange();
     String changeId = r.getChangeId();
@@ -538,6 +600,149 @@
   }
 
   @Test
+  public void pushCommitOfOtherUser() throws Exception {
+    // admin pushes commit of user
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    CommitInfo commit = change.revisions.get(change.currentRevision).commit;
+    assertThat(commit.author.email).isEqualTo(user.email);
+    assertThat(commit.committer.email).isEqualTo(user.email);
+
+    // check that the author/committer was added as reviewer
+    Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+    assertThat(change.reviewers.get(CC)).isNull();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.body())
+        .contains(admin.fullName + " has uploaded a new change for review");
+    assertThat(m.body())
+        .contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    assertMailFrom(m, admin.email);
+  }
+
+  @Test
+  public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception {
+    // create hidden project that is only visible to administrators
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.allow(cfg,
+        Permission.READ,
+        groupCache.get(new AccountGroup.NameKey("Administrators"))
+            .getGroupUUID(),
+        "refs/*");
+    Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    // admin pushes commit of user
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, user.getIdent(), repo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+    CommitInfo commit = change.revisions.get(change.currentRevision).commit;
+    assertThat(commit.author.email).isEqualTo(user.email);
+    assertThat(commit.committer.email).isEqualTo(user.email);
+
+    // check the user cannot see the change
+    setApiUser(user);
+    try {
+      gApi.changes().id(result.getChangeId()).get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException e) {
+      // Expected.
+    }
+
+    // check that the author/committer was NOT added as reviewer (he can't see
+    // the change)
+    assertThat(change.reviewers.get(REVIEWER)).isNull();
+    assertThat(change.reviewers.get(CC)).isNull();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
+  public void pushCommitWithFooterOfOtherUser() throws Exception {
+    // admin pushes commit that references 'user' in a footer
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT + "\n\n"
+            + FooterConstants.REVIEWED_BY.getName() + ": "
+            + user.getIdent().toExternalString(),
+        PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    // check that 'user' was added as reviewer
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER);
+    assertThat(reviewers).isNotNull();
+    assertThat(reviewers).hasSize(1);
+    assertThat(reviewers.iterator().next()._accountId)
+        .isEqualTo(user.getId().get());
+    assertThat(change.reviewers.get(CC)).isNull();
+
+    List<Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    Message m = messages.get(0);
+    assertThat(m.rcpt()).containsExactly(user.emailAddress);
+    assertThat(m.body()).contains("Hello " + user.fullName + ",\n");
+    assertThat(m.body()).contains("I'd like you to do a code review.");
+    assertThat(m.body())
+        .contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
+    assertMailFrom(m, admin.email);
+  }
+
+  @Test
+  public void pushCommitWithFooterOfOtherUserThatCannotSeeChange()
+      throws Exception {
+    // create hidden project that is only visible to administrators
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.allow(cfg,
+        Permission.READ, groupCache
+            .get(new AccountGroup.NameKey("Administrators")).getGroupUUID(),
+        "refs/*");
+    Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    // admin pushes commit that references 'user' in a footer
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo,
+        PushOneCommit.SUBJECT + "\n\n" + FooterConstants.REVIEWED_BY.getName()
+            + ": " + user.getIdent().toExternalString(),
+        PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    // check that 'user' cannot see the change
+    setApiUser(user);
+    try {
+      gApi.changes().id(result.getChangeId()).get();
+      fail("Expected ResourceNotFoundException");
+    } catch (ResourceNotFoundException e) {
+      // Expected.
+    }
+
+    // check that 'user' was NOT added as cc ('user' can't see the change)
+    setApiUser(admin);
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.reviewers.get(REVIEWER)).isNull();
+    assertThat(change.reviewers.get(CC)).isNull();
+    assertThat(sender.getMessages()).isEmpty();
+  }
+
+  @Test
   public void addReviewerThatCannotSeeChange() throws Exception {
     // create hidden project that is only visible to administrators
     Project.NameKey p = createProject("p");
@@ -577,6 +782,22 @@
   }
 
   @Test
+  public void addReviewerThatIsInactive() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    String username = name("new-user");
+    gApi.accounts().create(username).setActive(false);
+
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = username;
+    exception.expect(UnprocessableEntityException.class);
+    exception.expectMessage("Account of " + username + " is inactive.");
+    gApi.changes()
+        .id(r.getChangeId())
+        .addReviewer(in);
+  }
+
+  @Test
   public void addReviewer() throws Exception {
     TestTimeUtil.resetWithClockStep(1, SECONDS);
     PushOneCommit.Result r = createChange();
@@ -755,11 +976,18 @@
     assertThat(reviewers.iterator().next()._accountId)
         .isEqualTo(user.getId().get());
 
+    sender.clear();
     gApi.changes()
         .id(changeId)
         .reviewer(user.getId().toString())
         .remove();
-    assertThat(gApi.changes().id(changeId).get().reviewers.isEmpty());
+    assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
+
+    assertThat(sender.getMessages()).hasSize(1);
+    Message message = sender.getMessages().get(0);
+    assertThat(message.body()).contains(
+        "Removed reviewer " + user.fullName + ".");
+    assertThat(message.body()).doesNotContain("with the following votes");
 
     // Make sure the reviewer can still be added again.
     gApi.changes()
@@ -785,6 +1013,15 @@
 
   @Test
   public void removeReviewer() throws Exception {
+    testRemoveReviewer(true);
+  }
+
+  @Test
+  public void removeNoNotify() throws Exception {
+    testRemoveReviewer(false);
+  }
+
+  private void testRemoveReviewer(boolean notify) throws Exception {
     PushOneCommit.Result r = createChange();
     String changeId = r.getChangeId();
     gApi.changes()
@@ -810,11 +1047,26 @@
     assertThat(reviewerIt.next()._accountId)
         .isEqualTo(user.getId().get());
 
+    sender.clear();
     setApiUser(admin);
+    DeleteReviewerInput input = new DeleteReviewerInput();
+    if (!notify) {
+      input.notify = NotifyHandling.NONE;
+    }
     gApi.changes()
         .id(changeId)
         .reviewer(user.getId().toString())
-        .remove();
+        .remove(input);
+
+    if (notify) {
+      assertThat(sender.getMessages()).hasSize(1);
+      Message message = sender.getMessages().get(0);
+      assertThat(message.body()).contains(
+          "Removed reviewer " + user.fullName + " with the following votes");
+      assertThat(message.body()).contains("* Code-Review+1 by " + user.fullName);
+    } else {
+      assertThat(sender.getMessages()).hasSize(0);
+    }
 
     reviewers = gApi.changes()
         .id(changeId)
@@ -882,18 +1134,8 @@
         .reviewer(user.getId().toString())
         .votes();
 
-    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
-      // that the user is still returned as CC when all votes of that user have
-      // been deleted.
-      assertThat(m).containsEntry("Code-Review", Short.valueOf((short)0));
-    }
+    // Dummy 0 approval on the change to block vote copying to this patch set.
+    assertThat(m).containsExactly("Code-Review", Short.valueOf((short)0));
 
     ChangeInfo c = gApi.changes()
         .id(r.getChangeId())
@@ -1205,6 +1447,31 @@
   }
 
   @Test
+  public void submitStaleChange() throws Exception {
+    PushOneCommit.Result r = createChange();
+
+    disableChangeIndexWrites();
+    try {
+      r = amendChange(r.getChangeId());
+    } finally {
+      enableChangeIndexWrites();
+    }
+
+    gApi.changes()
+      .id(r.getChangeId())
+      .current()
+      .review(ReviewInput.approve());
+
+    gApi.changes()
+      .id(r.getChangeId())
+      .current()
+      .submit();
+    assertThat(gApi.changes()
+        .id(r.getChangeId())
+        .info().status).isEqualTo(ChangeStatus.MERGED);
+  }
+
+  @Test
   public void check() throws Exception {
     // TODO(dborowitz): Re-enable when ConsistencyChecker supports NoteDb.
     assume().that(notesMigration.enabled()).isFalse();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/GetMergeListIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/GetMergeListIT.java
new file mode 100644
index 0000000..0cb7d89
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/GetMergeListIT.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.common.CommitInfo;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+import java.util.List;
+
+@NoHttpd
+public class GetMergeListIT extends AbstractDaemonTest {
+
+  @Test
+  public void getMergeList() throws Exception {
+    ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+
+    PushOneCommit.Result gp1 = pushFactory
+        .create(db, admin.getIdent(), testRepo, "grand parent 1",
+            ImmutableMap.of("foo", "foo-1.1", "bar", "bar-1.1"))
+        .to("refs/for/master");
+
+    PushOneCommit.Result p1 = pushFactory
+        .create(db, admin.getIdent(), testRepo, "parent 1",
+            ImmutableMap.of("foo", "foo-1.2", "bar", "bar-1.2"))
+        .to("refs/for/master");
+
+    // reset HEAD in order to create a sibling of the first change
+    testRepo.reset(initial);
+
+    PushOneCommit.Result gp2 = pushFactory
+        .create(db, admin.getIdent(), testRepo, "grand parent 1",
+            ImmutableMap.of("foo", "foo-2.1", "bar", "bar-2.1"))
+        .to("refs/for/master");
+
+    PushOneCommit.Result p2 = pushFactory
+        .create(db, admin.getIdent(), testRepo, "parent 2",
+            ImmutableMap.of("foo", "foo-2.2", "bar", "bar-2.2"))
+        .to("refs/for/master");
+
+    PushOneCommit m = pushFactory.create(db, admin.getIdent(), testRepo,
+        "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+    m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
+    PushOneCommit.Result result = m.to("refs/for/master");
+    result.assertOkStatus();
+
+    List<CommitInfo> mergeList =
+        gApi.changes().id(result.getChangeId()).current().getMergeList().get();
+    assertThat(mergeList).hasSize(2);
+    assertThat(mergeList.get(0).commit).isEqualTo(p2.getCommit().name());
+    assertThat(mergeList.get(1).commit).isEqualTo(gp2.getCommit().name());
+
+    mergeList = gApi.changes().id(result.getChangeId()).current().getMergeList()
+        .withUninterestingParent(2).get();
+    assertThat(mergeList).hasSize(2);
+    assertThat(mergeList.get(0).commit).isEqualTo(p1.getCommit().name());
+    assertThat(mergeList.get(1).commit).isEqualTo(gp1.getCommit().name());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 54fe28f..4da22d3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -311,6 +311,27 @@
     assertVotes(c, user, 0, 0, REWORK);
   }
 
+  @Test
+  public void deleteStickyVote() throws Exception {
+    String label = "Code-Review";
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().get(label)
+        .setCopyMaxScore(true);
+    saveProjectConfig(project, cfg);
+
+
+    // Vote max score on PS1
+    String changeId = createChange(REWORK);
+    vote(admin, changeId, label, 2);
+    assertVotes(detailedChange(changeId), admin, label, 2, null);
+    updateChange(changeId, REWORK);
+    assertVotes(detailedChange(changeId), admin, label, 2, REWORK);
+
+    // Delete vote that was copied via sticky approval
+    deleteVote(admin, changeId, "Code-Review");
+    assertVotes(detailedChange(changeId), admin, label, 0, REWORK);
+  }
+
   private ChangeInfo detailedChange(String changeId) throws Exception {
     return gApi.changes().id(changeId)
         .get(EnumSet.of(ListChangesOption.DETAILED_LABELS,
@@ -495,6 +516,15 @@
     return c.revisions.get(c.currentRevision).kind;
   }
 
+  private void vote(TestAccount user, String changeId, String label, int vote)
+      throws Exception {
+    setApiUser(user);
+    gApi.changes()
+        .id(changeId)
+        .current()
+        .review(new ReviewInput().label(label, vote));
+  }
+
   private void vote(TestAccount user, String changeId, int codeReviewVote,
       int verifiedVote) throws Exception {
     setApiUser(user);
@@ -504,6 +534,15 @@
     gApi.changes().id(changeId).current().review(in);
   }
 
+  private void deleteVote(TestAccount user, String changeId, String label)
+      throws Exception {
+    setApiUser(user);
+    gApi.changes()
+        .id(changeId)
+        .reviewer(user.getId().toString())
+        .deleteVote(label);
+  }
+
   private void assertVotes(ChangeInfo c, TestAccount user, int codeReviewVote,
       int verifiedVote) {
     assertVotes(c, user, codeReviewVote, verifiedVote, null);
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 ee2dbfe..322cd4e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.HEAD;
@@ -44,8 +45,10 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.DiffInfo;
+import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -53,7 +56,6 @@
 import com.google.gerrit.extensions.restapi.ETagView;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.server.change.GetRevisionActions;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -62,13 +64,16 @@
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -511,17 +516,17 @@
   @Test
   public void files() throws Exception {
     PushOneCommit.Result r = createChange();
-    assertThat(Iterables.all(gApi.changes()
+    Map<String, FileInfo> files = gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
-        .files()
-        .keySet(), new Predicate<String>() {
-            @Override
-            public boolean apply(String file) {
-              return file.matches(FILE_NAME + '|' + Patch.COMMIT_MSG);
-            }
-         }))
-      .isTrue();
+        .files();
+    assertThat(files).hasSize(2);
+    assertThat(Iterables.all(files.keySet(), new Predicate<String>() {
+      @Override
+      public boolean apply(String file) {
+        return file.matches(FILE_NAME + '|' + COMMIT_MSG);
+      }
+    })).isTrue();
   }
 
   @Test
@@ -534,7 +539,7 @@
         .revision(r.getCommit().name())
         .files()
         .keySet()
-      ).containsExactly(Patch.COMMIT_MSG, "foo", "bar");
+      ).containsExactly(COMMIT_MSG, "foo", "bar");
 
     // list files against parent 1
     assertThat(gApi.changes()
@@ -542,7 +547,7 @@
         .revision(r.getCommit().name())
         .files(1)
         .keySet()
-      ).containsExactly(Patch.COMMIT_MSG, "bar");
+      ).containsExactly(COMMIT_MSG, "bar");
 
     // list files against parent 2
     assertThat(gApi.changes()
@@ -550,19 +555,30 @@
         .revision(r.getCommit().name())
         .files(2)
         .keySet()
-      ).containsExactly(Patch.COMMIT_MSG, "foo");
+      ).containsExactly(COMMIT_MSG, "foo");
   }
 
   @Test
   public void diff() throws Exception {
     PushOneCommit.Result r = createChange();
+    assertDiffForNewFile(r, FILE_NAME, FILE_CONTENT);
+    assertDiffForNewFile(r, COMMIT_MSG, r.getCommit().getFullMessage());
+  }
+
+  @Test
+  public void diffDeletedFile() throws Exception {
+    pushFactory.create(db, admin.getIdent(), testRepo)
+        .to("refs/heads/master");
+    PushOneCommit.Result r =
+        pushFactory.create(db, admin.getIdent(), testRepo)
+        .rm("refs/for/master");
     DiffInfo diff = gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
         .file(FILE_NAME)
         .diff();
-    assertThat(diff.metaA).isNull();
-    assertThat(diff.metaB.lines).isEqualTo(1);
+    assertThat(diff.metaA.lines).isEqualTo(1);
+    assertThat(diff.metaB).isNull();
   }
 
   @Test
@@ -610,15 +626,8 @@
   @Test
   public void content() throws Exception {
     PushOneCommit.Result r = createChange();
-    BinaryResult bin = gApi.changes()
-        .id(r.getChangeId())
-        .revision(r.getCommit().name())
-        .file(FILE_NAME)
-        .content();
-    ByteArrayOutputStream os = new ByteArrayOutputStream();
-    bin.writeTo(os);
-    String res = new String(os.toByteArray(), UTF_8);
-    assertThat(res).isEqualTo(FILE_CONTENT);
+    assertContent(r, FILE_NAME, FILE_CONTENT);
+    assertContent(r, COMMIT_MSG, r.getCommit().getFullMessage());
   }
 
   @Test
@@ -822,4 +831,88 @@
     assertThat(eTag).isNotEqualTo(oldETag);
     return eTag;
   }
+
+  private void assertContent(PushOneCommit.Result pushResult, String path,
+      String expectedContent) throws Exception {
+    BinaryResult bin = gApi.changes()
+        .id(pushResult.getChangeId())
+        .revision(pushResult.getCommit().name())
+        .file(path)
+        .content();
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    bin.writeTo(os);
+    String res = new String(os.toByteArray(), UTF_8);
+    assertThat(res).isEqualTo(expectedContent);
+  }
+
+  private void assertDiffForNewFile(PushOneCommit.Result pushResult, String path,
+      String expectedContentSideB) throws Exception {
+    DiffInfo diff = gApi.changes()
+        .id(pushResult.getChangeId())
+        .revision(pushResult.getCommit().name())
+        .file(path)
+        .diff();
+
+    List<String> expectedLines = new ArrayList<>();
+    if (path.equals(COMMIT_MSG)) {
+      RevCommit c = pushResult.getCommit();
+
+      RevCommit parentCommit = c.getParents()[0];
+      String parentCommitId = testRepo.getRevWalk().getObjectReader()
+          .abbreviate(parentCommit.getId(), 8).name();
+      expectedLines.add("Parent:     " + parentCommitId + " ("
+          + parentCommit.getShortMessage() + ")");
+
+      SimpleDateFormat dtfmt =
+          new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US);
+      PersonIdent author = c.getAuthorIdent();
+      dtfmt.setTimeZone(author.getTimeZone());
+      expectedLines.add("Author:     " + author.getName() + " <"
+          + author.getEmailAddress() + ">");
+      expectedLines.add("AuthorDate: "
+          + dtfmt.format(Long.valueOf(author.getWhen().getTime())));
+
+      PersonIdent committer = c.getCommitterIdent();
+      dtfmt.setTimeZone(committer.getTimeZone());
+      expectedLines.add("Commit:     " + committer.getName() + " <"
+          + committer.getEmailAddress() + ">");
+      expectedLines.add("CommitDate: "
+          + dtfmt.format(Long.valueOf(committer.getWhen().getTime())));
+      expectedLines.add("");
+    }
+
+    for (String line : expectedContentSideB.split("\n")) {
+      expectedLines.add(line);
+    }
+
+    assertThat(diff.binary).isNull();
+    assertThat(diff.changeType).isEqualTo(ChangeType.ADDED);
+    assertThat(diff.diffHeader).isNotNull();
+    assertThat(diff.intralineStatus).isNull();
+    assertThat(diff.webLinks).isNull();
+
+    assertThat(diff.metaA).isNull();
+    assertThat(diff.metaB).isNotNull();
+    assertThat(diff.metaB.commitId).isEqualTo(pushResult.getCommit().name());
+    assertThat(diff.metaB.contentType).isEqualTo(
+        path.equals(COMMIT_MSG)
+            ? "text/x-gerrit-commit-message"
+            : "text/plain");
+    assertThat(diff.metaB.lines).isEqualTo(expectedLines.size());
+    assertThat(diff.metaB.name).isEqualTo(path);
+    assertThat(diff.metaB.webLinks).isNull();
+
+    assertThat(diff.content).hasSize(1);
+    DiffInfo.ContentEntry contentEntry = diff.content.get(0);
+    assertThat(contentEntry.b).hasSize(expectedLines.size());
+    for (int i = 0; i < contentEntry.b.size(); i++) {
+      assertThat(contentEntry.b.get(i)).isEqualTo(expectedLines.get(i));
+    }
+    assertThat(contentEntry.a).isNull();
+    assertThat(contentEntry.ab).isNull();
+    assertThat(contentEntry.common).isNull();
+    assertThat(contentEntry.editA).isNull();
+    assertThat(contentEntry.editB).isNull();
+    assertThat(contentEntry.skip).isNull();
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index fdf18a6..affef17 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -31,6 +31,9 @@
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -161,7 +164,7 @@
     assertThat(
         modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
             RawInputUtil.create(CONTENT_NEW2))).isEqualTo(RefUpdate.Result.FORCED);
-    editUtil.publish(editUtil.byChange(change).get());
+    editUtil.publish(editUtil.byChange(change).get(), NotifyHandling.NONE);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(edit.isPresent()).isFalse();
     assertChangeMessages(change,
@@ -191,6 +194,24 @@
   }
 
   @Test
+  public void publishEditNotifyRest() throws Exception {
+    AddReviewerInput in = new AddReviewerInput();
+    in.reviewer = user.email;
+    gApi.changes().id(change.getChangeId()).addReviewer(in);
+
+    modifier.createEdit(change, getCurrentPatchSet(changeId));
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME,
+            RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+
+    sender.clear();
+    PublishChangeEditInput input = new PublishChangeEditInput();
+    input.notify = NotifyHandling.NONE;
+    adminRestSession.post(urlPublish(), input).assertNoContent();
+    assertThat(sender.getMessages()).hasSize(0);
+  }
+
+  @Test
   public void deleteEditRest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     assertThat(
@@ -354,7 +375,7 @@
     edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getFullMessage()).isEqualTo(msg);
 
-    editUtil.publish(edit.get());
+    editUtil.publish(edit.get(), NotifyHandling.NONE);
     assertThat(editUtil.byChange(change).isPresent()).isFalse();
 
     ChangeInfo info = get(changeId, ListChangesOption.CURRENT_COMMIT,
@@ -397,7 +418,7 @@
       assertThat(readContentFromJson(r)).isEqualTo(commit.getFullMessage());
     }
 
-    editUtil.publish(edit.get());
+    editUtil.publish(edit.get(), NotifyHandling.NONE);
     assertChangeMessages(change,
         ImmutableList.of("Uploaded patch set 1.",
             "Uploaded patch set 2.",
@@ -700,7 +721,7 @@
     assertThat(modifier.modifyMessage(edit.get(), newMsg))
         .isEqualTo(RefUpdate.Result.FORCED);
     edit = editUtil.byChange(change);
-    editUtil.publish(edit.get());
+    editUtil.publish(edit.get(), NotifyHandling.NONE);
 
     ChangeInfo info = get(changeId);
     assertThat(info.subject).isEqualTo(newSubj);
@@ -727,7 +748,7 @@
     editUtil.delete(editUtil.byChange(change).get());
     assertThat(queryEdits()).hasSize(1);
 
-    editUtil.publish(editUtil.byChange(change2).get());
+    editUtil.publish(editUtil.byChange(change2).get(), NotifyHandling.NONE);
     assertThat(queryEdits()).hasSize(0);
 
     setApiUser(user);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index ca669b4..b49bdba 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -129,6 +130,27 @@
   }
 
   @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void pushInitialCommitForMasterBranch() throws Exception {
+    RevCommit c =
+        testRepo.commit().message("Initial commit").insertChangeId().create();
+    String id = GitUtil.getChangeId(testRepo, c).get();
+    testRepo.reset(c);
+
+    String r = "refs/for/master";
+    PushResult pr = pushHead(testRepo, r, false);
+    assertPushOk(pr, r);
+
+    ChangeInfo change = gApi.changes().id(id).info();
+    assertThat(change.branch).isEqualTo("master");
+    assertThat(change.status).isEqualTo(ChangeStatus.NEW);
+
+    try (Repository repo = repoManager.openRepository(project)) {
+      assertThat(repo.resolve("master")).isNull();
+    }
+  }
+
+  @Test
   public void output() throws Exception {
     String url = canonicalWebUrl.get();
     ObjectId initialHead = testRepo.getRepository().resolve("HEAD");
@@ -176,6 +198,21 @@
   }
 
   @Test
+  public void pushForMasterWithTopicOption() throws Exception {
+    String topicOption = "topic=myTopic";
+    List<String> pushOptions = new ArrayList<>();
+    pushOptions.add(topicOption);
+
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+    push.setPushOptions(pushOptions);
+    PushOneCommit.Result r = push.to("refs/for/master");
+
+    r.assertOkStatus();
+    r.assertChange(Change.Status.NEW, "myTopic");
+    r.assertPushOptions(pushOptions);
+  }
+
+  @Test
   public void pushForMasterWithNotify() throws Exception {
     TestAccount user2 = accounts.user2();
     String pushSpec = "refs/for/master"
@@ -783,6 +820,23 @@
     pushWithReviewerInFooter("Notauser", null);
   }
 
+  @Test
+  public void pushNewPatchsetOverridingStickyLabel() throws Exception {
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    LabelType codeReview = Util.codeReview();
+    codeReview.setCopyMaxScore(true);
+    cfg.getLabelSections().put(codeReview.getName(), codeReview);
+    saveProjectConfig(cfg);
+
+    PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review+2");
+    r.assertOkStatus();
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
+            "b.txt", "anotherContent", r.getChangeId());
+    r = push.to("refs/for/master%l=Code-Review+1");
+    r.assertOkStatus();
+  }
+
   private void pushWithReviewerInFooter(String nameEmail,
       TestAccount expectedReviewer) throws Exception {
     int n = 5;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 848b428..ae2e8aa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -77,6 +77,7 @@
   @Test
   public void submitOnPushWithAnnotatedTag() throws Exception {
     grant(Permission.SUBMIT, project, "refs/for/refs/heads/master");
+    grant(Permission.PUSH, project, "refs/tags/*");
     PushOneCommit.AnnotatedTag tag =
         new PushOneCommit.AnnotatedTag("v1.0", "annotation", admin.getIdent());
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index c9c81df..1d53d66 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -20,6 +20,8 @@
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
+import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.junit.Assert.fail;
 
@@ -30,6 +32,7 @@
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
 import com.google.gerrit.extensions.api.projects.ProjectInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
@@ -52,12 +55,15 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.Util;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -116,6 +122,74 @@
   }
 
   @Test
+  public void submitNoPermission() throws Exception {
+    // create project where submit is blocked
+    Project.NameKey p = createProject("p");
+    block(Permission.SUBMIT, REGISTERED_USERS, "refs/*", p);
+
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    submit(result.getChangeId(), new SubmitInput(), AuthException.class,
+        "submit not permitted");
+  }
+
+  @Test
+  public void noSelfSubmit() throws Exception {
+    // create project where submit is blocked for the change owner
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.block(cfg, Permission.SUBMIT, CHANGE_OWNER, "refs/*");
+    Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
+    Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2,
+        REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+
+    submit(result.getChangeId(), new SubmitInput(), AuthException.class,
+        "submit not permitted");
+
+    setApiUser(user);
+    submit(result.getChangeId());
+  }
+
+  @Test
+  public void onlySelfSubmit() throws Exception {
+    // create project where only the change owner can submit
+    Project.NameKey p = createProject("p");
+    ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
+    Util.block(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/*");
+    Util.allow(cfg, Permission.SUBMIT, CHANGE_OWNER, "refs/*");
+    Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2,
+        REGISTERED_USERS, "refs/*");
+    saveProjectConfig(p, cfg);
+
+    TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
+    PushOneCommit.Result result = push.to("refs/for/master");
+    result.assertOkStatus();
+
+    ChangeInfo change = gApi.changes().id(result.getChangeId()).get();
+    assertThat(change.owner._accountId).isEqualTo(admin.id.get());
+
+    setApiUser(user);
+    submit(result.getChangeId(), new SubmitInput(), AuthException.class,
+        "submit not permitted");
+
+    setApiUser(admin);
+    submit(result.getChangeId());
+  }
+
+  @Test
   public void submitWholeTopic() throws Exception {
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
     PushOneCommit.Result change1 =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index aa7e864..b1b3f2a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -32,7 +32,9 @@
 import com.google.gerrit.extensions.api.changes.ReviewResult;
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.mail.Address;
@@ -43,8 +45,10 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 
 public class ChangeReviewersIT extends AbstractDaemonTest {
   @Test
@@ -260,6 +264,165 @@
   }
 
   @Test
+  public void driveByComment() throws Exception {
+    // Create change owned by admin.
+    PushOneCommit.Result r = createChange();
+
+    // Post drive-by message as user.
+    ReviewInput input = new ReviewInput().message("hello");
+    RestResponse resp = userRestSession.post(
+        "/changes/" + r.getChangeId() + "/revisions/" +
+        r.getCommit().getName() + "/review", input);
+    ReviewResult result = readContentFromJson(resp, 200, ReviewResult.class);
+    assertThat(result.labels).isNull();
+    assertThat(result.reviewers).isNull();
+
+    // Verify user is not added as reviewer.
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    assertReviewers(c, REVIEWER);
+    assertReviewers(c, CC);
+  }
+
+  @Test
+  public void addSelfAsReviewer() throws Exception {
+    // Create change owned by admin.
+    PushOneCommit.Result r = createChange();
+
+    // user adds self as REVIEWER.
+    ReviewInput input = new ReviewInput().reviewer(user.username);
+    RestResponse resp = userRestSession.post(
+        "/changes/" + r.getChangeId() + "/revisions/" +
+        r.getCommit().getName() + "/review", input);
+    ReviewResult result = readContentFromJson(resp, 200, ReviewResult.class);
+    assertThat(result.labels).isNull();
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(1);
+
+    // Verify reviewer state.
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    assertReviewers(c, REVIEWER, user);
+    assertReviewers(c, CC);
+    LabelInfo label = c.labels.get("Code-Review");
+    assertThat(label).isNotNull();
+    assertThat(label.all).isNotNull();
+    assertThat(label.all).hasSize(1);
+    ApprovalInfo approval = label.all.get(0);
+    assertThat(approval._accountId).isEqualTo(user.getId().get());
+  }
+
+  @Test
+  public void addSelfAsCc() throws Exception {
+    // Create change owned by admin.
+    PushOneCommit.Result r = createChange();
+
+    // user adds self as CC.
+    ReviewInput input = new ReviewInput().reviewer(user.username, CC, false);
+    RestResponse resp = userRestSession.post(
+        "/changes/" + r.getChangeId() + "/revisions/" +
+        r.getCommit().getName() + "/review", input);
+    ReviewResult result = readContentFromJson(resp, 200, ReviewResult.class);
+    assertThat(result.labels).isNull();
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(1);
+
+    // Verify reviewer state.
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    if (notesMigration.readChanges()) {
+      assertReviewers(c, REVIEWER);
+      assertReviewers(c, CC, user);
+      // Verify no approvals were added.
+      assertThat(c.labels).isNotNull();
+      LabelInfo label = c.labels.get("Code-Review");
+      assertThat(label).isNotNull();
+      assertThat(label.all).isNull();
+    } else {
+      // When approvals are stored in ReviewDb, we still create a label for
+      // the reviewing user, and force them into the REVIEWER state.
+      assertReviewers(c, REVIEWER, user);
+      assertReviewers(c, CC);
+      LabelInfo label = c.labels.get("Code-Review");
+      assertThat(label).isNotNull();
+      assertThat(label.all).isNotNull();
+      assertThat(label.all).hasSize(1);
+      ApprovalInfo approval = label.all.get(0);
+      assertThat(approval._accountId).isEqualTo(user.getId().get());
+    }
+  }
+
+  @Test
+  public void reviewerReplyWithoutVote() throws Exception {
+    // Create change owned by admin.
+    PushOneCommit.Result r = createChange();
+
+    // Verify reviewer state.
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    assertReviewers(c, REVIEWER);
+    assertReviewers(c, CC);
+    LabelInfo label = c.labels.get("Code-Review");
+    assertThat(label).isNotNull();
+    assertThat(label.all).isNull();
+
+    // Add user as REVIEWER.
+    ReviewInput input = new ReviewInput().reviewer(user.username);
+    ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input);
+    assertThat(result.labels).isNull();
+    assertThat(result.reviewers).isNotNull();
+    assertThat(result.reviewers).hasSize(1);
+
+    // Verify reviewer state. Both admin and user should be REVIEWERs now,
+    // because admin gets forced into REVIEWER state by virtue of being owner.
+    c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    assertReviewers(c, REVIEWER, admin, user);
+    assertReviewers(c, CC);
+    label = c.labels.get("Code-Review");
+    assertThat(label).isNotNull();
+    assertThat(label.all).isNotNull();
+    assertThat(label.all).hasSize(2);
+    Map<Integer, Integer> approvals = new HashMap<>();
+    for (ApprovalInfo approval : label.all) {
+      approvals.put(approval._accountId, approval.value);
+    }
+    assertThat(approvals).containsEntry(admin.getId().get(), 0);
+    assertThat(approvals).containsEntry(user.getId().get(), 0);
+
+    // Comment as user without voting. This should delete the approval and
+    // then replace it with the default value.
+    input = new ReviewInput().message("hello");
+    RestResponse resp = userRestSession.post(
+        "/changes/" + r.getChangeId() + "/revisions/" +
+        r.getCommit().getName() + "/review", input);
+    result = readContentFromJson(resp, 200, ReviewResult.class);
+    assertThat(result.labels).isNull();
+
+    // Verify reviewer state.
+    c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+    assertReviewers(c, REVIEWER, admin, user);
+    assertReviewers(c, CC);
+    label = c.labels.get("Code-Review");
+    assertThat(label).isNotNull();
+    assertThat(label.all).isNotNull();
+    assertThat(label.all).hasSize(2);
+    approvals.clear();
+    for (ApprovalInfo approval : label.all) {
+      approvals.put(approval._accountId, approval.value);
+    }
+    assertThat(approvals).containsEntry(admin.getId().get(), 0);
+    assertThat(approvals).containsEntry(user.getId().get(), 0);
+  }
+
+  @Test
   public void reviewAndAddReviewers() throws Exception {
     TestAccount observer = accounts.user2();
     PushOneCommit.Result r = createChange();
@@ -316,7 +479,7 @@
     assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress);
     assertThat(m.body()).contains(admin.fullName + " has posted comments on this change.");
     assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
-    assertThat(m.body()).contains("Patch Set 1: Code-Review+2\n");
+    assertThat(m.body()).contains("Patch Set 1: Code-Review+2");
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java
new file mode 100644
index 0000000..f5ae072
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD;
+import static com.google.common.net.HttpHeaders.ORIGIN;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.testutil.ConfigSuite;
+
+import org.apache.http.Header;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.message.BasicHeader;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class CorsIT extends AbstractDaemonTest {
+  @ConfigSuite.Default
+  public static Config allowExampleDotCom() {
+    Config cfg = new Config();
+    cfg.setStringList(
+        "site", null, "allowOriginRegex",
+        ImmutableList.of(
+            "https?://(.+[.])?example[.]com",
+            "http://friend[.]ly"));
+    return cfg;
+  }
+
+  @Test
+  public void origin() throws Exception {
+    Result change = createChange();
+
+    String url = "/changes/" + change.getChangeId() + "/detail";
+    RestResponse r = adminRestSession.get(url);
+    r.assertOK();
+    assertThat(r.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isNull();
+    assertThat(r.getHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS)).isNull();
+
+    check(url, true, "http://example.com");
+    check(url, true, "https://sub.example.com");
+    check(url, true, "http://friend.ly");
+
+    check(url, false, "http://evil.attacker");
+    check(url, false, "http://friendsly");
+  }
+
+  @Test
+  public void putWithOriginRefused() throws Exception {
+    Result change = createChange();
+    String origin = "http://example.com";
+    RestResponse r = adminRestSession.putWithHeader(
+        "/changes/" + change.getChangeId() + "/topic",
+        new BasicHeader(ORIGIN, origin),
+        "A");
+    r.assertOK();
+    checkCors(r, false, origin);
+  }
+
+  @Test
+  public void preflightOk() throws Exception {
+    Result change = createChange();
+
+    String origin = "http://example.com";
+    Request req = Request.Options(adminRestSession.url()
+        + "/a/changes/" + change.getChangeId() + "/detail");
+    req.addHeader(ORIGIN, origin);
+    req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "GET");
+    req.addHeader(ACCESS_CONTROL_REQUEST_HEADERS, "X-Requested-With");
+
+    RestResponse res = adminRestSession.execute(req);
+    res.assertOK();
+    checkCors(res, true, origin);
+  }
+
+  @Test
+  public void preflightBadOrigin() throws Exception {
+    Result change = createChange();
+
+    Request req = Request.Options(adminRestSession.url()
+        + "/a/changes/" + change.getChangeId() + "/detail");
+    req.addHeader(ORIGIN, "http://evil.attacker");
+    req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "GET");
+
+    adminRestSession.execute(req).assertBadRequest();
+  }
+
+  @Test
+  public void preflightBadMethod() throws Exception {
+    Result change = createChange();
+
+    for (String method : new String[] {"POST", "PUT", "DELETE", "PATCH"}) {
+      Request req = Request.Options(adminRestSession.url()
+          + "/a/changes/" + change.getChangeId() + "/detail");
+      req.addHeader(ORIGIN, "http://example.com");
+      req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, method);
+      adminRestSession.execute(req).assertBadRequest();
+    }
+  }
+
+  @Test
+  public void preflightBadHeader() throws Exception {
+    Result change = createChange();
+
+    Request req = Request.Options(adminRestSession.url()
+        + "/a/changes/" + change.getChangeId() + "/detail");
+    req.addHeader(ORIGIN, "http://example.com");
+    req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "GET");
+    req.addHeader(ACCESS_CONTROL_REQUEST_HEADERS, "X-Gerrit-Auth");
+
+    adminRestSession.execute(req).assertBadRequest();
+  }
+
+  private RestResponse check(String url, boolean accept, String origin)
+      throws Exception {
+    Header hdr = new BasicHeader(ORIGIN, origin);
+    RestResponse r = adminRestSession.getWithHeader(url, hdr);
+    r.assertOK();
+    checkCors(r, accept, origin);
+    return r;
+  }
+
+  private void checkCors(RestResponse r, boolean accept, String origin) {
+    String allowOrigin = r.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN);
+    String allowCred = r.getHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS);
+    String allowMethods = r.getHeader(ACCESS_CONTROL_ALLOW_METHODS);
+    String allowHeaders = r.getHeader(ACCESS_CONTROL_ALLOW_HEADERS);
+    if (accept) {
+      assertThat(allowOrigin).isEqualTo(origin);
+      assertThat(allowCred).isEqualTo("true");
+      assertThat(allowMethods).isEqualTo("GET, OPTIONS");
+      assertThat(allowHeaders).isEqualTo("X-Requested-With");
+    } else {
+      assertThat(allowOrigin).isNull();
+      assertThat(allowCred).isNull();
+      assertThat(allowMethods).isNull();
+      assertThat(allowHeaders).isNull();
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index d5b6f14..ce7e76d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -47,7 +48,7 @@
 @NoHttpd
 public class SubmitResolvingMergeCommitIT extends AbstractDaemonTest {
   @Inject
-  private MergeSuperSet mergeSuperSet;
+  private Provider<MergeSuperSet> mergeSuperSet;
 
   @Inject
   private Submit submit;
@@ -293,7 +294,7 @@
       throws MissingObjectException, IncorrectObjectTypeException, IOException,
       OrmException {
     ChangeSet cs =
-        mergeSuperSet.completeChangeSet(db, change.change(), user(admin));
+        mergeSuperSet.get().completeChangeSet(db, change.change(), user(admin));
     assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 54fa74c..b0280e8 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -20,19 +20,20 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GerritConfigs;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
-import com.google.gerrit.server.config.GetServerInfo.ServerInfo;
 
 import org.junit.Test;
 
 import java.nio.file.Files;
 import java.nio.file.Path;
 
+@NoHttpd
 public class ServerInfoIT extends AbstractDaemonTest {
 
   @Test
@@ -74,12 +75,12 @@
     @GerritConfig(name = "user.anonymousCoward", value = "Unnamed User"),
   })
   public void serverConfig() throws Exception {
-    ServerInfo i = getServerConfig();
+    ServerInfo i = gApi.config().server().getInfo();
 
     // auth
     assertThat(i.auth.authType).isEqualTo(AuthType.HTTP);
     assertThat(i.auth.editableAccountFields).containsExactly(
-        Account.FieldName.REGISTER_NEW_EMAIL, Account.FieldName.FULL_NAME);
+        AccountFieldName.REGISTER_NEW_EMAIL, AccountFieldName.FULL_NAME);
     assertThat(i.auth.useContributorAgreements).isTrue();
     assertThat(i.auth.loginUrl).isEqualTo("https://example.com/login");
     assertThat(i.auth.loginText).isEqualTo("LOGIN");
@@ -121,9 +122,9 @@
 
     // notedb
     notesMigration.setReadChanges(true);
-    assertThat(getServerConfig().noteDbEnabled).isTrue();
+    assertThat(gApi.config().server().getInfo().noteDbEnabled).isTrue();
     notesMigration.setReadChanges(false);
-    assertThat(getServerConfig().noteDbEnabled).isNull();
+    assertThat(gApi.config().server().getInfo().noteDbEnabled).isNull();
   }
 
   @Test
@@ -134,7 +135,7 @@
     Files.write(jsplugin, "Gerrit.install(function(self){});\n".getBytes(UTF_8));
     adminSshSession.exec("gerrit plugin reload");
 
-    ServerInfo i = getServerConfig();
+    ServerInfo i = gApi.config().server().getInfo();
 
     // plugin
     assertThat(i.plugin.jsResourcePaths).hasSize(1);
@@ -142,13 +143,13 @@
 
   @Test
   public void serverConfigWithDefaults() throws Exception {
-    ServerInfo i = getServerConfig();
+    ServerInfo i = gApi.config().server().getInfo();
 
     // auth
     assertThat(i.auth.authType).isEqualTo(AuthType.OPENID);
     assertThat(i.auth.editableAccountFields).containsExactly(
-        Account.FieldName.REGISTER_NEW_EMAIL, Account.FieldName.FULL_NAME,
-        Account.FieldName.USER_NAME);
+        AccountFieldName.REGISTER_NEW_EMAIL, AccountFieldName.FULL_NAME,
+        AccountFieldName.USER_NAME);
     assertThat(i.auth.useContributorAgreements).isNull();
     assertThat(i.auth.loginUrl).isNull();
     assertThat(i.auth.loginText).isNull();
@@ -189,9 +190,12 @@
     assertThat(i.user.anonymousCowardName).isEqualTo(AnonymousCowardNameProvider.DEFAULT);
   }
 
-  private ServerInfo getServerConfig() throws Exception {
-    RestResponse r = adminRestSession.get("/config/server/info/");
-    r.assertOK();
-    return newGson().fromJson(r.getReader(), ServerInfo.class);
+  @Test
+  @GerritConfig(name = "auth.contributorAgreements", value = "true")
+  public void anonymousAccess() throws Exception {
+    configureContributorAgreement(true);
+
+    setApiUserAnonymous();
+    gApi.config().server().getInfo();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 46f93b6..6377710 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -67,9 +67,9 @@
   }
 
   @Test
-  public void createBranchByAdminCreateReferenceBlocked() throws Exception {
+  public void createBranchByAdminCreateReferenceBlocked_Forbidden() throws Exception {
     blockCreateReference();
-    assertCreateSucceeds();
+    assertCreateFails(AuthException.class);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 955e580..1c9711f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -36,6 +36,7 @@
 
   @Before
   public void setUp() throws Exception {
+    project = createProject(name("p"));
     branch = new Branch.NameKey(project, "test");
     branch().create(new BranchInput());
   }
@@ -73,10 +74,32 @@
     assertDeleteForbidden();
   }
 
+  @Test
+  public void deleteBranchByUserWithForcePushPermission() throws Exception {
+    grantForcePush();
+    setApiUser(user);
+    assertDeleteSucceeds();
+  }
+
+  @Test
+  public void deleteBranchByUserWithDeletePermission() throws Exception {
+    grantDelete();
+    setApiUser(user);
+    assertDeleteSucceeds();
+  }
+
   private void blockForcePush() throws Exception {
     block(Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true);
   }
 
+  private void grantForcePush() throws Exception {
+    grant(Permission.PUSH, project, "refs/heads/*", true, ANONYMOUS_USERS);
+  }
+
+  private void grantDelete() throws Exception {
+    allow(Permission.DELETE, ANONYMOUS_USERS, "refs/*");
+  }
+
   private void grantOwner() throws Exception {
     allow(Permission.OWNER, REGISTERED_USERS, "refs/*");
   }
@@ -99,6 +122,7 @@
 
   private void assertDeleteForbidden() throws Exception {
     exception.expect(AuthException.class);
+    exception.expectMessage("Cannot delete branch");
     branch().delete();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java
new file mode 100644
index 0000000..01a2443
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java
@@ -0,0 +1,290 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.createAnnotatedTag;
+import static com.google.gerrit.acceptance.GitUtil.deleteRef;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.GitUtil.updateAnnotatedTag;
+import static com.google.gerrit.acceptance.rest.project.PushTagIT.TagType.ANNOTATED;
+import static com.google.gerrit.acceptance.rest.project.PushTagIT.TagType.LIGHTWEIGHT;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.common.base.MoreObjects;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.client.RefNames;
+
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class PushTagIT extends AbstractDaemonTest {
+  enum TagType {
+    LIGHTWEIGHT(Permission.CREATE),
+    ANNOTATED(Permission.CREATE_TAG);
+
+    final String createPermission;
+
+    TagType(String createPermission) {
+      this.createPermission = createPermission;
+    }
+  }
+
+  private RevCommit initialHead;
+
+  @Before
+  public void setup() throws Exception {
+    // clone with user to avoid inherited tag permissions of admin user
+    testRepo = cloneProject(project, user);
+
+    initialHead = getRemoteHead();
+  }
+
+  @Test
+  public void createTagForExistingCommit() throws Exception {
+    for (TagType tagType : TagType.values()) {
+      pushTagForExistingCommit(tagType, Status.REJECTED_OTHER_REASON);
+
+      allowTagCreation(tagType);
+      pushTagForExistingCommit(tagType, Status.OK);
+
+      allowPushOnRefsTags();
+      pushTagForExistingCommit(tagType, Status.OK);
+
+      removePushFromRefsTags();
+    }
+  }
+
+  @Test
+  public void createTagForNewCommit() throws Exception {
+    for (TagType tagType : TagType.values()) {
+      pushTagForNewCommit(tagType, Status.REJECTED_OTHER_REASON);
+
+      allowTagCreation(tagType);
+      pushTagForNewCommit(tagType, Status.REJECTED_OTHER_REASON);
+
+      allowPushOnRefsTags();
+      pushTagForNewCommit(tagType, Status.OK);
+
+      removePushFromRefsTags();
+    }
+  }
+
+  @Test
+  public void fastForward() throws Exception {
+    for (TagType tagType : TagType.values()) {
+      allowTagCreation(tagType);
+      String tagName = pushTagForExistingCommit(tagType, Status.OK);
+
+      fastForwardTagToExistingCommit(tagType, tagName,
+          Status.REJECTED_OTHER_REASON);
+      fastForwardTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON);
+
+      allowTagDeletion();
+      fastForwardTagToExistingCommit(tagType, tagName,
+          Status.REJECTED_OTHER_REASON);
+      fastForwardTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON);
+
+      allowPushOnRefsTags();
+      Status expectedStatus =
+          tagType == ANNOTATED ? Status.REJECTED_OTHER_REASON : Status.OK;
+      fastForwardTagToExistingCommit(tagType, tagName, expectedStatus);
+      fastForwardTagToNewCommit(tagType, tagName, expectedStatus);
+
+      allowForcePushOnRefsTags();
+      fastForwardTagToExistingCommit(tagType, tagName, Status.OK);
+      fastForwardTagToNewCommit(tagType, tagName, Status.OK);
+
+      removePushFromRefsTags();
+    }
+  }
+
+  @Test
+  public void forceUpdate() throws Exception {
+    for (TagType tagType : TagType.values()) {
+      allowTagCreation(tagType);
+      String tagName = pushTagForExistingCommit(tagType, Status.OK);
+
+      forceUpdateTagToExistingCommit(tagType, tagName,
+          Status.REJECTED_OTHER_REASON);
+      forceUpdateTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON);
+
+      allowPushOnRefsTags();
+      forceUpdateTagToExistingCommit(tagType, tagName,
+          Status.REJECTED_OTHER_REASON);
+      forceUpdateTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON);
+
+      allowTagDeletion();
+      forceUpdateTagToExistingCommit(tagType, tagName,
+          Status.REJECTED_OTHER_REASON);
+      forceUpdateTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON);
+
+      allowForcePushOnRefsTags();
+      forceUpdateTagToExistingCommit(tagType, tagName, Status.OK);
+      forceUpdateTagToNewCommit(tagType, tagName, Status.OK);
+
+      removePushFromRefsTags();
+    }
+  }
+
+  @Test
+  public void delete() throws Exception {
+    for (TagType tagType : TagType.values()) {
+      allowTagCreation(tagType);
+      String tagName = pushTagForExistingCommit(tagType, Status.OK);
+
+      pushTagDeletion(tagType, tagName, Status.REJECTED_OTHER_REASON);
+
+      allowPushOnRefsTags();
+      pushTagDeletion(tagType, tagName, Status.REJECTED_OTHER_REASON);
+    }
+
+    allowForcePushOnRefsTags();
+    for (TagType tagType : TagType.values()) {
+      String tagName = pushTagForExistingCommit(tagType, Status.OK);
+      pushTagDeletion(tagType, tagName, Status.OK);
+    }
+
+    removePushFromRefsTags();
+    allowTagDeletion();
+    for (TagType tagType : TagType.values()) {
+      String tagName = pushTagForExistingCommit(tagType, Status.OK);
+      pushTagDeletion(tagType, tagName, Status.OK);
+    }
+  }
+
+  private String pushTagForExistingCommit(TagType tagType,
+      Status expectedStatus) throws Exception {
+    return pushTag(tagType, null, false, false, expectedStatus);
+  }
+
+  private String pushTagForNewCommit(TagType tagType,
+      Status expectedStatus) throws Exception {
+    return pushTag(tagType, null, true, false, expectedStatus);
+  }
+
+  private void fastForwardTagToExistingCommit(TagType tagType, String tagName,
+      Status expectedStatus) throws Exception {
+    pushTag(tagType, tagName, false, false, expectedStatus);
+  }
+
+  private void fastForwardTagToNewCommit(TagType tagType, String tagName,
+      Status expectedStatus) throws Exception {
+    pushTag(tagType, tagName, true, false, expectedStatus);
+  }
+
+  private void forceUpdateTagToExistingCommit(TagType tagType, String tagName,
+      Status expectedStatus) throws Exception {
+    pushTag(tagType, tagName, false, true, expectedStatus);
+  }
+
+  private void forceUpdateTagToNewCommit(TagType tagType, String tagName,
+      Status expectedStatus) throws Exception {
+    pushTag(tagType, tagName, true, true, expectedStatus);
+  }
+
+  private String pushTag(TagType tagType, String tagName, boolean newCommit,
+      boolean force, Status expectedStatus) throws Exception {
+    if (force) {
+      testRepo.reset(initialHead);
+    }
+    commit(user.getIdent(), "subject");
+
+    boolean createTag = tagName == null;
+    tagName = MoreObjects.firstNonNull(tagName, "v1" + "_" + System.nanoTime());
+    switch (tagType) {
+      case LIGHTWEIGHT:
+        break;
+      case ANNOTATED:
+        if (createTag) {
+          createAnnotatedTag(testRepo, tagName, user.getIdent());
+        } else {
+          updateAnnotatedTag(testRepo, tagName, user.getIdent());
+        }
+        break;
+      default:
+        throw new IllegalStateException("unexpected tag type: " + tagType);
+    }
+
+    if (!newCommit) {
+      grant(Permission.SUBMIT, project, "refs/for/refs/heads/master", false,
+          REGISTERED_USERS);
+      pushHead(testRepo, "refs/for/master%submit");
+    }
+
+    String tagRef = tagRef(tagName);
+    PushResult r = tagType == LIGHTWEIGHT
+        ? pushHead(testRepo, tagRef, false, force)
+        : GitUtil.pushTag(testRepo, tagName, !createTag);
+    RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef);
+    assertThat(refUpdate.getStatus())
+        .named(tagType.name())
+        .isEqualTo(expectedStatus);
+    return tagName;
+  }
+
+  private void pushTagDeletion(TagType tagType, String tagName,
+      Status expectedStatus) throws Exception {
+    String tagRef = tagRef(tagName);
+    PushResult r = deleteRef(testRepo, tagRef);
+    RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef);
+    assertThat(refUpdate.getStatus()).named(tagType.name())
+        .isEqualTo(expectedStatus);
+  }
+
+  private void allowTagCreation(TagType tagType) throws Exception {
+    grant(tagType.createPermission, project, "refs/tags/*", false,
+        REGISTERED_USERS);
+  }
+
+  private void allowPushOnRefsTags() throws Exception {
+    removePushFromRefsTags();
+    grant(Permission.PUSH, project, "refs/tags/*", false, REGISTERED_USERS);
+  }
+
+  private void allowForcePushOnRefsTags() throws Exception {
+    removePushFromRefsTags();
+    grant(Permission.PUSH, project, "refs/tags/*", true, REGISTERED_USERS);
+  }
+
+  private void allowTagDeletion() throws Exception {
+    removePushFromRefsTags();
+    grant(Permission.DELETE, project, "refs/tags/*", true, REGISTERED_USERS);
+  }
+
+  private void removePushFromRefsTags() throws Exception {
+    removePermission(Permission.PUSH, project, "refs/tags/*");
+  }
+
+  private void commit(PersonIdent ident, String subject) throws Exception {
+    commitBuilder()
+        .ident(ident)
+        .message(subject + " (" + System.nanoTime() + ")")
+        .create();
+  }
+
+  private static String tagRef(String tagName) {
+    return RefNames.REFS_TAGS + tagName;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
index 33aa726..c4aee29 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -238,6 +238,7 @@
 
   @Test
   public void createTagNotAllowed() throws Exception {
+    block(Permission.CREATE, REGISTERED_USERS, R_TAGS + "*");
     TagInput input = new TagInput();
     input.ref = "test";
     exception.expect(AuthException.class);
@@ -247,7 +248,7 @@
 
   @Test
   public void createAnnotatedTagNotAllowed() throws Exception {
-    block(Permission.PUSH_TAG, REGISTERED_USERS, R_TAGS + "*");
+    block(Permission.CREATE_TAG, REGISTERED_USERS, R_TAGS + "*");
     TagInput input = new TagInput();
     input.ref = "test";
     input.message = "annotation";
@@ -338,8 +339,8 @@
 
   private void grantTagPermissions() throws Exception {
     grant(Permission.CREATE, project, R_TAGS + "*");
-    grant(Permission.PUSH_TAG, project, R_TAGS + "*");
-    grant(Permission.PUSH_SIGNED_TAG, project, R_TAGS + "*");
+    grant(Permission.CREATE_TAG, project, R_TAGS + "*");
+    grant(Permission.CREATE_SIGNED_TAG, project, R_TAGS + "*");
   }
 
   private ListRefsRequest<TagInfo> getTags() throws Exception {
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 d9f1a5c..70dafaa 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
@@ -544,6 +544,7 @@
         + "\n"
         + "PS2, Line 2: nten\n"
         + "typo: content\n"
+        + "\n"
         + "\n");
   }
 
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 37e551f..e33d163 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
@@ -796,7 +796,7 @@
       ins = patchSetInserterFactory.create(ctl, nextPatchSetId(ctl), commit)
           .setValidatePolicy(CommitValidators.Policy.NONE)
           .setFireRevisionCreated(false)
-          .setSendMail(false);
+          .setNotify(NotifyHandling.NONE);
       bu.addOp(ctl.getId(), ins).execute();
     }
     return reload(ctl);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 40ea296..37ced5f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.common.CommitInfo;
@@ -646,6 +647,39 @@
         changeAndCommit(psId1_1, c1_1, 1));
   }
 
+  @Test
+  public void getRelatedForStaleChange() throws Exception {
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "1")
+        .message("subject: 1")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+
+    RevCommit c2_2 = testRepo.amend(c2_1)
+        .add("b.txt", "2")
+        .create();
+    testRepo.reset(c2_2);
+
+    disableChangeIndexWrites();
+    try {
+      pushHead(testRepo, "refs/for/master", false);
+    } finally {
+      enableChangeIndexWrites();
+    }
+
+    PatchSet.Id psId1_1 = getPatchSetId(c1_1);
+    PatchSet.Id psId2_1 = getPatchSetId(c2_1);
+    PatchSet.Id psId2_2 = new PatchSet.Id(psId2_1.changeId, psId2_1.get() + 1);
+
+    assertRelated(psId2_2, changeAndCommit(psId2_2, c2_2, 2),
+        changeAndCommit(psId1_1, c1_1, 1));
+  }
+
   private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws Exception {
     return getRelated(ps.getParentKey(), ps.get());
   }
@@ -654,8 +688,9 @@
       throws Exception {
     String url = String.format("/changes/%d/revisions/%d/related",
         changeId.get(), ps);
-    return newGson().fromJson(adminRestSession.get(url).getReader(),
-        RelatedInfo.class).changes;
+    RestResponse r = adminRestSession.get(url);
+    r.assertOK();
+    return newGson().fromJson(r.getReader(), RelatedInfo.class).changes;
   }
 
   private RevCommit parseBody(RevCommit c) throws Exception {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index 752f0d2..afd6734 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -16,13 +16,11 @@
 
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.common.RpcImpl;
 import com.google.gwtjsonrpc.common.RpcImpl.Version;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.List;
 import java.util.Set;
@@ -36,14 +34,4 @@
   @SignInRequired
   void deleteExternalIds(Set<AccountExternalId.Key> keys,
       AsyncCallback<Set<AccountExternalId.Key>> callback);
-
-  @Audit
-  @SignInRequired
-  void updateContact(String fullName, String emailAddr,
-      AsyncCallback<Account> callback);
-
-  @Audit
-  @SignInRequired
-  void enterAgreement(String agreementName,
-      AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
deleted file mode 100644
index 22482c7..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2008 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.common.auth.SignInRequired;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-
-@RpcImpl(version = Version.V2_0)
-public interface AccountService extends RemoteJsonService {
-  @SignInRequired
-  void myAgreements(AsyncCallback<AgreementInfo> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 97f11b4..8fae4a6 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -24,6 +24,9 @@
   public static final String ABANDON = "abandon";
   public static final String ADD_PATCH_SET = "addPatchSet";
   public static final String CREATE = "create";
+  public static final String DELETE = "delete";
+  public static final String CREATE_TAG = "createTag";
+  public static final String CREATE_SIGNED_TAG = "createSignedTag";
   public static final String DELETE_DRAFTS = "deleteDrafts";
   public static final String EDIT_HASHTAGS = "editHashtags";
   public static final String EDIT_TOPIC_NAME = "editTopicName";
@@ -36,8 +39,6 @@
   public static final String PUBLISH_DRAFTS = "publishDrafts";
   public static final String PUSH = "push";
   public static final String PUSH_MERGE = "pushMerge";
-  public static final String PUSH_TAG = "pushTag";
-  public static final String PUSH_SIGNED_TAG = "pushSignedTag";
   public static final String READ = "read";
   public static final String REBASE = "rebase";
   public static final String REMOVE_REVIEWER = "removeReviewer";
@@ -46,8 +47,8 @@
   public static final String VIEW_DRAFTS = "viewDrafts";
 
   private static final List<String> NAMES_LC;
-  private static final int labelIndex;
-  private static final int labelAsIndex;
+  private static final int LABEL_INDEX;
+  private static final int LABEL_AS_INDEX;
 
   static {
     NAMES_LC = new ArrayList<>();
@@ -56,13 +57,14 @@
     NAMES_LC.add(ABANDON.toLowerCase());
     NAMES_LC.add(ADD_PATCH_SET.toLowerCase());
     NAMES_LC.add(CREATE.toLowerCase());
+    NAMES_LC.add(CREATE_TAG.toLowerCase());
+    NAMES_LC.add(CREATE_SIGNED_TAG.toLowerCase());
+    NAMES_LC.add(DELETE.toLowerCase());
     NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
     NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
     NAMES_LC.add(FORGE_SERVER.toLowerCase());
     NAMES_LC.add(PUSH.toLowerCase());
     NAMES_LC.add(PUSH_MERGE.toLowerCase());
-    NAMES_LC.add(PUSH_TAG.toLowerCase());
-    NAMES_LC.add(PUSH_SIGNED_TAG.toLowerCase());
     NAMES_LC.add(LABEL.toLowerCase());
     NAMES_LC.add(LABEL_AS.toLowerCase());
     NAMES_LC.add(REBASE.toLowerCase());
@@ -75,8 +77,8 @@
     NAMES_LC.add(DELETE_DRAFTS.toLowerCase());
     NAMES_LC.add(PUBLISH_DRAFTS.toLowerCase());
 
-    labelIndex = NAMES_LC.indexOf(Permission.LABEL);
-    labelAsIndex = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase());
+    LABEL_INDEX = NAMES_LC.indexOf(Permission.LABEL);
+    LABEL_AS_INDEX = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase());
   }
 
   /** @return true if the name is recognized as a permission name. */
@@ -247,9 +249,9 @@
 
   private static int index(Permission a) {
     if (isLabel(a.getName())) {
-      return labelIndex;
+      return LABEL_INDEX;
     } else if (isLabelAs(a.getName())) {
-      return labelAsIndex;
+      return LABEL_AS_INDEX;
     }
 
     int index = NAMES_LC.indexOf(a.getName().toLowerCase());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
index 272801f..fb54ef1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -29,8 +28,5 @@
   @AllowCrossSiteRequest
   void daemonHostKeys(AsyncCallback<List<SshHostKey>> callback);
 
-  @SignInRequired
-  void contributorAgreements(AsyncCallback<List<ContributorAgreement>> callback);
-
   void clientError(String message, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK
index 61cd406..2cc8291 100644
--- a/gerrit-extension-api/BUCK
+++ b/gerrit-extension-api/BUCK
@@ -74,7 +74,7 @@
   name = 'extension-api-javadoc',
   title = 'Gerrit Review Extension API Documentation',
   pkgs = ['com.google.gerrit.extensions'],
-  paths = ['src/main/java'],
+  source_jar = ':extension-api-src',
   srcs = SRCS,
   deps = [
     '//lib:guava',
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 023362f..7375893 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index c1cb3ec..9765bbf 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -34,6 +34,9 @@
 public interface AccountApi {
   AccountInfo get() throws RestApiException;
 
+  boolean getActive() throws RestApiException;
+  void setActive(boolean active) throws RestApiException;
+
   String getAvatarUrl(int size) throws RestApiException;
 
   GeneralPreferencesInfo getPreferences() throws RestApiException;
@@ -85,6 +88,16 @@
     }
 
     @Override
+    public boolean getActive() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public void setActive(boolean active) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public String getAvatarUrl(int size) throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java
new file mode 100644
index 0000000..6af0dbb
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java
@@ -0,0 +1,21 @@
+// 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.extensions.api.changes;
+
+/** Input passed to {@code DELETE /changes/[id]/reviewers/[id]}. */
+public class DeleteReviewerInput {
+  /** Who to send email notifications to after the reviewer is deleted. */
+  public NotifyHandling notify = NotifyHandling.ALL;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/PublishChangeEditInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/PublishChangeEditInput.java
new file mode 100644
index 0000000..fa6f18f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/PublishChangeEditInput.java
@@ -0,0 +1,21 @@
+// 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.extensions.api.changes;
+
+/** Input passed to {@code POST /changes/[id]/edit:publish/}. */
+public class PublishChangeEditInput {
+  /** Who to send email notifications to after the change edit is published. */
+  public NotifyHandling notify = NotifyHandling.ALL;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
index d1f09e8..79cc12e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
@@ -25,6 +25,7 @@
   void deleteVote(String label) throws RestApiException;
   void deleteVote(DeleteVoteInput input) throws RestApiException;
   void remove() throws RestApiException;
+  void remove(DeleteReviewerInput input) throws RestApiException;
 
   /**
    * A default implementation which allows source compatibility
@@ -50,5 +51,10 @@
     public void remove() throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public void remove(DeleteReviewerInput input) throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 2731476..20dda19 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.common.TestSubmitRuleInput;
@@ -72,6 +73,33 @@
   SubmitType submitType() throws RestApiException;
   SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException;
 
+  MergeListRequest getMergeList() throws RestApiException;
+
+  abstract class MergeListRequest {
+    private boolean addLinks;
+    private int uninterestingParent = 1;
+
+    public abstract List<CommitInfo> get() throws RestApiException;
+
+    public MergeListRequest withLinks() {
+      this.addLinks = true;
+      return this;
+    }
+
+    public MergeListRequest withUninterestingParent(int uninterestingParent) {
+      this.uninterestingParent = uninterestingParent;
+      return this;
+    }
+
+    public boolean getAddLinks() {
+      return addLinks;
+    }
+
+    public int getUninterestingParent() {
+      return uninterestingParent;
+    }
+  }
+
   /**
    * A default implementation which allows source compatibility
    * when adding new methods to the interface.
@@ -217,5 +245,10 @@
         throws RestApiException {
       throw new NotImplementedException();
     }
+
+    @Override
+    public MergeListRequest getMergeList() throws RestApiException {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
index a43c29f..1e5c95e 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 
@@ -25,6 +26,8 @@
    */
   String getVersion() throws RestApiException;
 
+  ServerInfo getInfo() throws RestApiException;
+
   GeneralPreferencesInfo getDefaultPreferences() throws RestApiException;
   GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in)
       throws RestApiException;
@@ -43,6 +46,11 @@
     }
 
     @Override
+    public ServerInfo getInfo() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public GeneralPreferencesInfo getDefaultPreferences()
         throws RestApiException {
       throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java
new file mode 100644
index 0000000..07d9f37
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java
@@ -0,0 +1,19 @@
+// 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.extensions.client;
+
+public enum AccountFieldName {
+  FULL_NAME, USER_NAME, REGISTER_NEW_EMAIL
+}
\ No newline at end of file
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
similarity index 91%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
index 38a78ba..2056e25 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
@@ -12,13 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb.client;
+package com.google.gerrit.extensions.client;
 
 public enum AuthType {
-  /** Login relies upon the OpenID standard: {@link "http://openid.net/"} */
+  /** Login relies upon the <a href="http://openid.net/">OpenID standard</a> */
   OPENID,
 
-  /** Login relies upon the OpenID standard: {@link "http://openid.net/"} in Single Sign On mode */
+  /** Login relies upon the <a href="http://openid.net/">OpenID standard</a> in Single Sign On mode */
   OPENID_SSO,
 
   /**
@@ -49,7 +49,7 @@
    * Jetty's SSL channel to request client's SSL certificate. For this
    * authentication to work a Gerrit administrator has to import the root
    * certificate of the trust chain used to issue the client's certificate
-   * into the <review-site>/etc/keystore.
+   * into the &lt;review-site&gt;/etc/keystore.
    * <p>
    * After the authentication is done Gerrit will obtain basic user
    * registration (name and email) from LDAP, and some group memberships.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 9754f12..ec9190a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -83,6 +83,25 @@
     DISABLED
   }
 
+  public enum DefaultBase {
+    AUTO_MERGE(null),
+    FIRST_PARENT(-1);
+
+    private final String base;
+
+    DefaultBase(String base) {
+      this.base = base;
+    }
+
+    DefaultBase(int base) {
+      this(Integer.toString(base));
+    }
+
+    public String getBase() {
+      return base;
+    }
+  }
+
   public enum TimeFormat {
     /** 12-hour clock: 1:15 am, 2:13 pm */
     HHMM_12("h:mm a"),
@@ -123,6 +142,7 @@
   public List<MenuItem> my;
   public Map<String, String> urlAliases;
   public EmailStrategy emailStrategy;
+  public DefaultBase defaultBaseForMerges;
 
   public boolean isShowInfoInReviewCategory() {
     return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
@@ -180,6 +200,7 @@
     p.legacycidInChangeTable = false;
     p.muteCommonPathPrefixes = true;
     p.signedOffBy = false;
+    p.defaultBaseForMerges = DefaultBase.FIRST_PARENT;
     return p;
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java
index 6ec5b1d..4242fcd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java
@@ -18,4 +18,5 @@
   public String name;
   public String description;
   public String url;
+  public GroupInfo autoVerifyGroup;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java
new file mode 100644
index 0000000..1000e9c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java
@@ -0,0 +1,35 @@
+// 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.extensions.common;
+
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
+
+import java.util.List;
+
+public class AuthInfo {
+  public AuthType authType;
+  public Boolean useContributorAgreements;
+  public List<AgreementInfo> contributorAgreements;
+  public List<AccountFieldName> editableAccountFields;
+  public String loginUrl;
+  public String loginText;
+  public String switchAccountUrl;
+  public String registerUrl;
+  public String registerText;
+  public String editFullNameUrl;
+  public String httpPasswordUrl;
+  public Boolean isGitBasicAuth;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
new file mode 100644
index 0000000..206b2f0
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class ChangeConfigInfo {
+  public Boolean allowBlame;
+  public Boolean allowDrafts;
+  public int largeChange;
+  public String replyLabel;
+  public String replyTooltip;
+  public int updateDelay;
+  public Boolean submitWholeTopic;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java
new file mode 100644
index 0000000..180e2d2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java
@@ -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.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+import java.util.Map;
+
+public class DownloadInfo {
+  public Map<String, DownloadSchemeInfo> schemes;
+  public List<String> archives;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
new file mode 100644
index 0000000..0e8ad65
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.Map;
+
+public class DownloadSchemeInfo {
+  public String url;
+  public Boolean isAuthRequired;
+  public Boolean isAuthSupported;
+  public Map<String, String> commands;
+  public Map<String, String> cloneCommands;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java
new file mode 100644
index 0000000..72c474f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+public class GerritInfo {
+  public String allProjects;
+  public String allUsers;
+  public Boolean docSearch;
+  public String docUrl;
+  public Boolean editGpgKeys;
+  public String reportBugUrl;
+  public String reportBugText;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java
new file mode 100644
index 0000000..845f7cb7
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java
@@ -0,0 +1,22 @@
+// 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.extensions.common;
+
+import java.util.List;
+
+public class PluginConfigInfo {
+  public Boolean hasAvatars;
+  public List<String> jsResourcePaths;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java
new file mode 100644
index 0000000..e66c242
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java
@@ -0,0 +1,19 @@
+// 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.extensions.common;
+
+public class ReceiveInfo {
+  public Boolean enableSignedPush;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java
new file mode 100644
index 0000000..3dd8368
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java
@@ -0,0 +1,31 @@
+// 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.extensions.common;
+
+import java.util.Map;
+
+public class ServerInfo {
+  public AuthInfo auth;
+  public ChangeConfigInfo change;
+  public DownloadInfo download;
+  public GerritInfo gerrit;
+  public Boolean noteDbEnabled;
+  public PluginConfigInfo plugin;
+  public SshdInfo sshd;
+  public SuggestInfo suggest;
+  public Map<String, String> urlAliases;
+  public UserConfigInfo user;
+  public ReceiveInfo receive;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java
new file mode 100644
index 0000000..98d650c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java
@@ -0,0 +1,18 @@
+// 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.extensions.common;
+
+public class SshdInfo {
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java
new file mode 100644
index 0000000..5b0dcbe
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java
@@ -0,0 +1,19 @@
+// 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.extensions.common;
+
+public class SuggestInfo {
+  public int from;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java
new file mode 100644
index 0000000..5010689
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java
@@ -0,0 +1,19 @@
+// 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.extensions.common;
+
+public class UserConfigInfo {
+  public String anonymousCowardName;
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java
index 40b84a3..d18f3e5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java
@@ -15,14 +15,11 @@
 package com.google.gerrit.extensions.events;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.extensions.common.AccountInfo;
 
 /** Notified whenever a Change is abandoned. */
 @ExtensionPoint
 public interface ChangeAbandonedListener {
   interface Event extends RevisionEvent {
-    @Deprecated
-    AccountInfo getAbandoner();
     String getReason();
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java
index d0ca6d6..de74a86 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java
@@ -15,14 +15,11 @@
 package com.google.gerrit.extensions.events;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.extensions.common.AccountInfo;
 
 /** Notified whenever a Change is merged. */
 @ExtensionPoint
 public interface ChangeMergedListener {
   interface Event extends RevisionEvent {
-    @Deprecated
-    AccountInfo getMerger();
     /**
      * Represents the merged Revision when the submit strategy is cherry-pick or
      * rebase-if-necessary.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java
index e5f3330..f533339 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java
@@ -15,14 +15,11 @@
 package com.google.gerrit.extensions.events;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.extensions.common.AccountInfo;
 
 /** Notified whenever a Change is restored. */
 @ExtensionPoint
 public interface ChangeRestoredListener {
   interface Event extends RevisionEvent {
-    @Deprecated
-    AccountInfo getRestorer();
     String getReason();
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java
index 6c82034..e8388a9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.extensions.events;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 
 import java.util.Map;
@@ -24,8 +23,6 @@
 @ExtensionPoint
 public interface CommentAddedListener {
   interface Event extends RevisionEvent {
-    @Deprecated
-    AccountInfo getAuthor();
     String getComment();
     Map<String, ApprovalInfo> getApprovals();
     Map<String, ApprovalInfo> getOldApprovals();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java
index 3857468..1fc574b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java
@@ -15,14 +15,11 @@
 package com.google.gerrit.extensions.events;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.extensions.common.AccountInfo;
 
 /** Notified whenever a Draft is published. */
 @ExtensionPoint
 public interface DraftPublishedListener {
   interface Event extends RevisionEvent {
-    @Deprecated
-    AccountInfo getPublisher();
   }
 
   void onDraftPublished(Event event);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java
index c49b0f3..ad13267 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.extensions.events;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.extensions.common.AccountInfo;
 
 import java.util.Collection;
 
@@ -23,8 +22,6 @@
 @ExtensionPoint
 public interface HashtagsEditedListener {
   interface Event extends ChangeEvent {
-    @Deprecated
-    AccountInfo getEditor();
     Collection<String> getHashtags();
     Collection<String> getAddedHashtags();
     Collection<String> getRemovedHashtags();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
index 3cc3fdc..bb4ac9d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
@@ -17,12 +17,14 @@
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.extensions.common.AccountInfo;
 
-/** Notified whenever a Reviewer is added to a change. */
+import java.util.List;
+
+/** Notified whenever one or more Reviewers are added to a change. */
 @ExtensionPoint
 public interface ReviewerAddedListener {
   interface Event extends ChangeEvent {
-    AccountInfo getReviewer();
+    List<AccountInfo> getReviewers();
   }
 
-  void onReviewerAdded(Event event);
+  void onReviewersAdded(Event event);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java
index 5e4e095..8d148b7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java
@@ -15,14 +15,11 @@
 package com.google.gerrit.extensions.events;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.extensions.common.AccountInfo;
 
 /** Notified whenever a Change Revision is created. */
 @ExtensionPoint
 public interface RevisionCreatedListener {
   interface Event extends RevisionEvent {
-    @Deprecated
-    AccountInfo getUploader();
   }
 
   void onRevisionCreated(Event event);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java
index 68ba22c..0c36d9d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java
@@ -15,14 +15,11 @@
 package com.google.gerrit.extensions.events;
 
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
-import com.google.gerrit.extensions.common.AccountInfo;
 
 /** Notified whenever a Change Topic is changed. */
 @ExtensionPoint
 public interface TopicEditedListener {
   interface Event extends ChangeEvent {
-    @Deprecated
-    AccountInfo getEditor();
     String getOldTopic();
   }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
index 068d9a0..4fc9ab6 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -86,12 +86,6 @@
   }
 
   /** Set the character set used to encode text data and return {@code this}. */
-  @Deprecated
-  public BinaryResult setCharacterEncoding(String encoding) {
-    return setCharacterEncoding(Charset.forName(encoding));
-  }
-
-  /** Set the character set used to encode text data and return {@code this}. */
   public BinaryResult setCharacterEncoding(Charset encoding) {
     characterEncoding = encoding;
     return this;
@@ -235,7 +229,7 @@
     StringResult(String str) {
       super(str.getBytes(UTF_8));
       setContentType("text/plain");
-      setCharacterEncoding(UTF_8.name());
+      setCharacterEncoding(UTF_8);
       this.str = str;
     }
 
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java
new file mode 100644
index 0000000..5fb2f48
--- /dev/null
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java
@@ -0,0 +1,27 @@
+// 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.client.info;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class AgreementInfo extends JavaScriptObject {
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String description() /*-{ return this.description; }-*/;
+  public final native String url() /*-{ return this.url; }-*/;
+  public final native GroupInfo autoVerifyGroup() /*-{ return this.auto_verify_group; }-*/;
+
+  protected AgreementInfo() {
+  }
+}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
index 0e3c32b..8669dd5 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.client.info;
 
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.core.client.JsArrayString;
 
 import java.util.ArrayList;
@@ -52,22 +52,30 @@
     return authType() == AuthType.CUSTOM_EXTENSION;
   }
 
-  public final boolean canEdit(Account.FieldName f) {
+  public final boolean canEdit(AccountFieldName f) {
     return editableAccountFields().contains(f);
   }
 
-  public final List<Account.FieldName> editableAccountFields() {
-    List<Account.FieldName> fields = new ArrayList<>();
+  public final List<AccountFieldName> editableAccountFields() {
+    List<AccountFieldName> fields = new ArrayList<>();
     for (String f : Natives.asList(_editableAccountFields())) {
-      fields.add(Account.FieldName.valueOf(f));
+      fields.add(AccountFieldName.valueOf(f));
     }
     return fields;
   }
 
+  public final List<AgreementInfo> contributorAgreements() {
+    List<AgreementInfo> agreements = new ArrayList<>();
+    for (AgreementInfo a : Natives.asList(_contributorAgreements())) {
+      agreements.add(a);
+    }
+    return agreements;
+  }
+
   public final boolean siteHasUsernames() {
     if (isCustomExtension()
         && httpPasswordUrl() != null
-        && !canEdit(FieldName.USER_NAME)) {
+        && !canEdit(AccountFieldName.USER_NAME)) {
       return false;
     }
     return true;
@@ -93,6 +101,8 @@
   private native String authTypeRaw() /*-{ return this.auth_type; }-*/;
   private native JsArrayString _editableAccountFields()
   /*-{ return this.editable_account_fields; }-*/;
+  private native JsArray<AgreementInfo> _contributorAgreements()
+  /*-{ return this.contributor_agreements; }-*/;
 
   protected AuthInfo() {
   }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
index 45953cb..9e25f25 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DefaultBase;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
@@ -55,6 +56,7 @@
     p.reviewCategoryStrategy(d.getReviewCategoryStrategy());
     p.diffView(d.getDiffView());
     p.emailStrategy(d.emailStrategy);
+    p.defaultBaseForMerges(d.defaultBaseForMerges);
     return p;
   }
 
@@ -135,6 +137,14 @@
   private native String emailStrategyRaw()
   /*-{ return this.email_strategy }-*/;
 
+  public final DefaultBase defaultBaseForMerges() {
+    String s = defaultBaseForMergesRaw();
+    return s != null ? DefaultBase.valueOf(s) : null;
+  }
+
+  private native String defaultBaseForMergesRaw()
+  /*-{ return this.default_base_for_merges }-*/;
+
   public final native JsArray<TopMenuItem> my()
   /*-{ return this.my; }-*/;
 
@@ -201,6 +211,12 @@
   private native void emailStrategyRaw(String s)
   /*-{ this.email_strategy = s }-*/;
 
+  public final void defaultBaseForMerges(DefaultBase b) {
+    defaultBaseForMergesRaw(b != null ? b.toString() : null);
+  }
+  private native void defaultBaseForMergesRaw(String b)
+  /*-{ this.default_base_for_merges = b }-*/;
+
   public final void setMyMenus(List<TopMenuItem> myMenus) {
     initMy();
     for (TopMenuItem n : myMenus) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
similarity index 95%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
index 4811e59..deed44d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.groups;
+package com.google.gerrit.client.info;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.core.client.JavaScriptObject;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
similarity index 95%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
index c3fd4ed..fa051a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
@@ -12,9 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.client.groups;
+package com.google.gerrit.client.info;
 
-import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
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 b7405c7..539d53b 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
@@ -89,7 +89,7 @@
 import com.google.gerrit.client.documentation.DocScreen;
 import com.google.gerrit.client.editor.EditScreen;
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.Screen;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
index acd2e78..9aca859 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.AgreementInfo;
 import com.google.gerrit.client.info.GpgKeyInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.NativeMap;
@@ -83,6 +84,14 @@
     new RestApi("/accounts/").id(account).view("name").get(cb);
   }
 
+  /** Set the account name */
+  public static void setName(String account, String name,
+      AsyncCallback<NativeString> cb) {
+    AccountNameInput input = AccountNameInput.create();
+    input.name(name);
+    new RestApi("/accounts/").id(account).view("name").put(input, cb);
+  }
+
   /** Retrieve email addresses */
   public static void getEmails(String account,
       AsyncCallback<JsArray<EmailInfo>> cb) {
@@ -97,6 +106,13 @@
         .ifNoneMatch().put(in, cb);
   }
 
+  /** Set preferred email address */
+  public static void setPreferredEmail(String account, String email,
+      AsyncCallback<NativeString> cb) {
+    new RestApi("/accounts/").id(account).view("emails")
+        .id(email).view("preferred").put(cb);
+  }
+
   /** Retrieve SSH keys */
   public static void getSshKeys(String account,
       AsyncCallback<JsArray<SshKeyInfo>> cb) {
@@ -196,6 +212,14 @@
     new RestApi("/accounts/").id(account).view("password.http").delete(cb);
   }
 
+  /** Enter a contributor agreement */
+  public static void enterAgreement(String account, String name,
+      AsyncCallback<NativeString> cb) {
+    AgreementInput in = AgreementInput.create();
+    in.name(name);
+    new RestApi("/accounts/").id(account).view("agreements").put(in, cb);
+  }
+
   private static JsArray<ProjectWatchInfo> projectWatchArrayFromSet(
       Set<ProjectWatchInfo> set) {
     JsArray<ProjectWatchInfo> jsArray = JsArray.createArray().cast();
@@ -205,6 +229,17 @@
     return jsArray;
   }
 
+  private static class AgreementInput extends JavaScriptObject {
+    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
+
+    static AgreementInput create() {
+      return createObject().cast();
+    }
+
+    protected AgreementInput() {
+    }
+  }
+
   private static class HttpPasswordInput extends JavaScriptObject {
     final native void generate(boolean g) /*-{ if(g)this.generate=g; }-*/;
 
@@ -227,6 +262,17 @@
     }
   }
 
+  private static class AccountNameInput extends JavaScriptObject {
+    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
+
+    static AccountNameInput create() {
+      return createObject().cast();
+    }
+
+    protected AccountNameInput() {
+    }
+  }
+
   public static void addGpgKey(String account, String armored,
       AsyncCallback<NativeMap<GpgKeyInfo>> cb) {
     new RestApi("/accounts/")
@@ -243,6 +289,12 @@
       .post(GpgKeysInput.delete(fingerprints), cb);
   }
 
+  /** List contributor agreements */
+  public static void getAgreements(String account,
+      AsyncCallback<JsArray<AgreementInfo>> cb) {
+    new RestApi("/accounts/").id(account).view("agreements").get(cb);
+  }
+
   private static class GpgKeysInput extends JavaScriptObject {
     static GpgKeysInput add(String key) {
       return createWithAdd(Natives.arrayOf(key));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index a084612..c58d6b5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -141,11 +141,8 @@
   String errorDialogTitleRegisterNewEmail();
 
   String newAgreement();
-  String agreementStatus();
   String agreementName();
   String agreementDescription();
-  String agreementStatus_EXPIRED();
-  String agreementStatus_VERIFIED();
 
   String newAgreementSelectTypeHeading();
   String newAgreementNoneAvailable();
@@ -171,4 +168,8 @@
   String messageCCMeOnMyComments();
   String messageDisabled();
   String emailFieldLabel();
+
+  String defaultBaseForMerges();
+  String autoMerge();
+  String firstParent();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index ca2d316..cc15aae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -19,6 +19,10 @@
 messageCCMeOnMyComments = CC Me On Comments I Write
 messageDisabled = Disabled
 
+defaultBaseForMerges = Default Base For Merges:
+autoMerge = Auto Merge
+firstParent = First Parent
+
 maximumPageSizeFieldLabel = Maximum Page Size:
 diffViewLabel = Diff View:
 dateFormatLabel = Date/Time Format:
@@ -151,10 +155,7 @@
 
 
 newAgreement = New Contributor Agreement
-agreementStatus = Status
 agreementName = Name
-agreementStatus_EXPIRED = Expired
-agreementStatus_VERIFIED = Verified
 agreementDescription = Description
 
 newAgreementSelectTypeHeading = Select an agreement type:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index ae3599d..f5f38fb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.CallbackGroup;
@@ -25,8 +24,7 @@
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
@@ -46,7 +44,6 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 
 class ContactPanelShort extends Composite {
   protected final FlowPanel body;
@@ -104,7 +101,7 @@
     }
 
     int row = 0;
-    if (!Gerrit.info().auth().canEdit(FieldName.USER_NAME)
+    if (!Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME)
         && Gerrit.info().auth().siteHasUsernames()) {
       infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
       row(infoPlainText, row++, Util.C.userName(), new UsernameField());
@@ -146,7 +143,7 @@
     save.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        doSave(null);
+        doSave();
       }
     });
 
@@ -173,11 +170,11 @@
   }
 
   private boolean canEditFullName() {
-    return Gerrit.info().auth().canEdit(Account.FieldName.FULL_NAME);
+    return Gerrit.info().auth().canEdit(AccountFieldName.FULL_NAME);
   }
 
   private boolean canRegisterNewEmail() {
-    return Gerrit.info().auth().canEdit(Account.FieldName.REGISTER_NEW_EMAIL);
+    return Gerrit.info().auth().canEdit(AccountFieldName.REGISTER_NEW_EMAIL);
   }
 
   void hideSaveButton() {
@@ -347,10 +344,13 @@
     inEmail.setFocus(true);
   }
 
-  void doSave(final AsyncCallback<Account> onSave) {
-    String newName = canEditFullName() ? nameTxt.getText() : null;
-    if (newName != null && newName.trim().isEmpty()) {
+  void doSave() {
+    final String newName;
+    String name = canEditFullName() ? nameTxt.getText() : null;
+    if (name != null && name.trim().isEmpty()) {
       newName = null;
+    } else {
+      newName = name;
     }
 
     final String newEmail;
@@ -368,24 +368,40 @@
     save.setEnabled(false);
     registerNewEmail.setEnabled(false);
 
-    Util.ACCOUNT_SEC.updateContact(newName, newEmail,
-        new GerritCallback<Account>() {
-          @Override
-          public void onSuccess(Account result) {
-            registerNewEmail.setEnabled(true);
-            onSaveSuccess(FormatUtil.asInfo(result));
-            if (onSave != null) {
-              onSave.onSuccess(result);
-            }
-          }
+    CallbackGroup group = new CallbackGroup();
+    if (!newEmail.equals(currentEmail)) {
+      AccountApi.setPreferredEmail("self", newEmail,
+          group.add(new GerritCallback<NativeString>() {
+        @Override
+        public void onSuccess(NativeString result) {
+        }
+      }));
+    }
+    AccountApi.setName("self", newName,
+        group.add(new GerritCallback<NativeString>() {
+      @Override
+      public void onSuccess(NativeString result) {
+      }
 
-          @Override
-          public void onFailure(final Throwable caught) {
-            save.setEnabled(true);
-            registerNewEmail.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
+      @Override
+      public void onFailure(Throwable caught) {
+        save.setEnabled(true);
+        registerNewEmail.setEnabled(true);
+        super.onFailure(caught);
+      }
+    }));
+    group.done();
+    group.addListener(new GerritCallback<Void>() {
+      @Override
+      public void onSuccess(Void result) {
+        currentEmail = newEmail;
+        AccountInfo me = Gerrit.getUserAccount();
+        me.email(currentEmail);
+        me.name(newName);
+        onSaveSuccess(me);
+        registerNewEmail.setEnabled(true);
+      }
+    });
   }
 
   void onSaveSuccess(AccountInfo result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index 308cf30..cd7c141 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -15,15 +15,19 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.AgreementInfo;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 
+import java.util.List;
+
 public class MyAgreementsScreen extends SettingsScreen {
   private AgreementTable agreements;
 
@@ -39,10 +43,11 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SVC.myAgreements(new ScreenLoadCallback<AgreementInfo>(this) {
+    AccountApi.getAgreements(
+        "self", new ScreenLoadCallback<JsArray<AgreementInfo>>(this) {
       @Override
-      public void preDisplay(final AgreementInfo result) {
-        agreements.display(result);
+      public void preDisplay(JsArray<AgreementInfo> result) {
+        agreements.display(Natives.asList(result));
       }
     });
   }
@@ -50,60 +55,43 @@
   private static class AgreementTable extends FancyFlexTable<ContributorAgreement> {
     AgreementTable() {
       table.setWidth("");
-      table.setText(0, 1, Util.C.agreementStatus());
-      table.setText(0, 2, Util.C.agreementName());
-      table.setText(0, 3, Util.C.agreementDescription());
+      table.setText(0, 1, Util.C.agreementName());
+      table.setText(0, 2, Util.C.agreementDescription());
 
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c < 4; c++) {
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      for (int c = 1; c < 3; c++) {
         fmt.addStyleName(0, c, Gerrit.RESOURCES.css().dataHeader());
       }
     }
 
-    void display(final AgreementInfo result) {
+    void display(List<AgreementInfo> result) {
       while (1 < table.getRowCount()) {
         table.removeRow(table.getRowCount() - 1);
       }
 
-      for (final String k : result.accepted) {
-        addOne(result, k);
+      for (AgreementInfo info : result) {
+        addOne(info);
       }
     }
 
-    void addOne(final AgreementInfo info, final String k) {
-      final int row = table.getRowCount();
+    void addOne(AgreementInfo info) {
+      int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
 
-      final ContributorAgreement cla = info.agreements.get(k);
-      final String statusName;
-      if (cla == null) {
-        statusName = Util.C.agreementStatus_EXPIRED();
+      String url = info.url();
+      if (url != null && url.length() > 0) {
+        Anchor a = new Anchor(info.name(), url);
+        a.setTarget("_blank");
+        table.setWidget(row, 1, a);
       } else {
-        statusName = Util.C.agreementStatus_VERIFIED();
+        table.setText(row, 1, info.name());
       }
-      table.setText(row, 1, statusName);
-
-      if (cla == null) {
-        table.setText(row, 2, "");
-        table.setText(row, 3, "");
-      } else {
-        final String url = cla.getAgreementUrl();
-        if (url != null && url.length() > 0) {
-          final Anchor a = new Anchor(cla.getName(), url);
-          a.setTarget("_blank");
-          table.setWidget(row, 2, a);
-        } else {
-          table.setText(row, 2, cla.getName());
-        }
-        table.setText(row, 3, cla.getDescription());
-      }
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c < 4; c++) {
+      table.setText(row, 2, info.description());
+      FlexCellFormatter fmt = table.getFlexCellFormatter();
+      for (int c = 1; c < 3; c++) {
         fmt.addStyleName(row, c, Gerrit.RESOURCES.css().dataCell());
       }
-
-      setRowItem(row, cla);
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 2b01b59..9433b04 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -61,6 +61,7 @@
   private ListBox reviewCategoryStrategy;
   private ListBox diffView;
   private ListBox emailStrategy;
+  private ListBox defaultBaseForMerges;
   private StringListPanel myMenus;
   private Button save;
 
@@ -106,6 +107,12 @@
             GeneralPreferencesInfo.EmailStrategy.DISABLED
                 .name());
 
+    defaultBaseForMerges = new ListBox();
+    defaultBaseForMerges.addItem(Util.C.autoMerge(),
+        GeneralPreferencesInfo.DefaultBase.AUTO_MERGE.name());
+    defaultBaseForMerges.addItem(Util.C.firstParent(),
+        GeneralPreferencesInfo.DefaultBase.FIRST_PARENT.name());
+
     diffView = new ListBox();
     diffView.addItem(
         com.google.gerrit.client.changes.Util.C.sideBySide(),
@@ -156,7 +163,7 @@
     signedOffBy = new CheckBox(Util.C.signedOffBy());
 
     boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled();
-    final Grid formGrid = new Grid(12 + (flashClippy ? 1 : 0), 2);
+    final Grid formGrid = new Grid(13 + (flashClippy ? 1 : 0), 2);
 
     int row = 0;
 
@@ -176,6 +183,10 @@
     formGrid.setWidget(row, fieldIdx, emailStrategy);
     row++;
 
+    formGrid.setText(row, labelIdx, Util.C.defaultBaseForMerges());
+    formGrid.setWidget(row, fieldIdx, defaultBaseForMerges);
+    row++;
+
     formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
     formGrid.setWidget(row, fieldIdx, diffView);
     row++;
@@ -239,6 +250,7 @@
     e.listenTo(diffView);
     e.listenTo(reviewCategoryStrategy);
     e.listenTo(emailStrategy);
+    e.listenTo(defaultBaseForMerges);
   }
 
   @Override
@@ -272,6 +284,7 @@
     reviewCategoryStrategy.setEnabled(on);
     diffView.setEnabled(on);
     emailStrategy.setEnabled(on);
+    defaultBaseForMerges.setEnabled(on);
   }
 
   private void display(GeneralPreferences p) {
@@ -296,6 +309,9 @@
     setListBox(emailStrategy,
         GeneralPreferencesInfo.EmailStrategy.ENABLED,
         p.emailStrategy());
+    setListBox(defaultBaseForMerges,
+        GeneralPreferencesInfo.DefaultBase.FIRST_PARENT,
+        p.defaultBaseForMerges());
     display(p.my());
   }
 
@@ -385,6 +401,10 @@
         GeneralPreferencesInfo.EmailStrategy.ENABLED,
         GeneralPreferencesInfo.EmailStrategy.values()));
 
+    p.defaultBaseForMerges(getListBox(defaultBaseForMerges,
+        GeneralPreferencesInfo.DefaultBase.FIRST_PARENT,
+        GeneralPreferencesInfo.DefaultBase.values()));
+
     List<TopMenuItem> items = new ArrayList<>();
     for (List<String> v : myMenus.getValues()) {
       items.add(TopMenuItem.create(v.get(0), v.get(1)));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index 14f8e2f..e7fa14c5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -16,14 +16,16 @@
 
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.info.AgreementInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountScreen;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.http.client.Request;
@@ -41,7 +43,6 @@
 import com.google.gwt.user.client.ui.RadioButton;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.HashSet;
 import java.util.List;
@@ -50,8 +51,8 @@
 public class NewAgreementScreen extends AccountScreen {
   private final String nextToken;
   private Set<String> mySigned;
-  private List<ContributorAgreement> available;
-  private ContributorAgreement current;
+  private List<AgreementInfo> available;
+  private AgreementInfo current;
 
   private VerticalPanel radios;
 
@@ -73,25 +74,22 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SVC.myAgreements(new GerritCallback<AgreementInfo>() {
+    AccountApi.getAgreements(
+        "self", new GerritCallback<JsArray<AgreementInfo>>() {
       @Override
-      public void onSuccess(AgreementInfo result) {
+      public void onSuccess(JsArray<AgreementInfo> result) {
         if (isAttached()) {
-          mySigned = new HashSet<>(result.accepted);
+          mySigned = new HashSet<>();
+          for (AgreementInfo info: Natives.asList(result)) {
+            mySigned.add(info.name());
+          }
           postRPC();
         }
       }
     });
-    Gerrit.SYSTEM_SVC
-        .contributorAgreements(new GerritCallback<List<ContributorAgreement>>() {
-          @Override
-          public void onSuccess(final List<ContributorAgreement> result) {
-            if (isAttached()) {
-              available = result;
-              postRPC();
-            }
-          }
-        });
+
+    available = Gerrit.info().auth().contributorAgreements();
+    postRPC();
   }
 
   @Override
@@ -158,12 +156,12 @@
     }
     radios.add(hdr);
 
-    for (final ContributorAgreement cla : available) {
-      final RadioButton r = new RadioButton("cla_id", cla.getName());
+    for (final AgreementInfo cla : available) {
+      final RadioButton r = new RadioButton("cla_id", cla.name());
       r.addStyleName(Gerrit.RESOURCES.css().contributorAgreementButton());
       radios.add(r);
 
-      if (mySigned.contains(cla.getName())) {
+      if (mySigned.contains(cla.name())) {
         r.setEnabled(false);
         final Label l = new Label(Util.C.newAgreementAlreadySubmitted());
         l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementAlreadySubmitted());
@@ -177,8 +175,8 @@
         });
       }
 
-      if (cla.getDescription() != null && !cla.getDescription().equals("")) {
-        final Label l = new Label(cla.getDescription());
+      if (cla.description() != null && !cla.description().equals("")) {
+        final Label l = new Label(cla.description());
         l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementShortDescription());
         radios.add(l);
       }
@@ -199,24 +197,24 @@
   }
 
   private void doEnterAgreement() {
-    Util.ACCOUNT_SEC.enterAgreement(current.getName(),
-        new GerritCallback<VoidResult>() {
+    AccountApi.enterAgreement("self", current.name(),
+        new GerritCallback<NativeString>() {
           @Override
-          public void onSuccess(final VoidResult result) {
+          public void onSuccess(NativeString result) {
             Gerrit.display(nextToken);
           }
 
           @Override
-          public void onFailure(final Throwable caught) {
+          public void onFailure(Throwable caught) {
             yesIAgreeBox.setText("");
             super.onFailure(caught);
           }
         });
   }
 
-  private void showCLA(final ContributorAgreement cla) {
+  private void showCLA(AgreementInfo cla) {
     current = cla;
-    String url = cla.getAgreementUrl();
+    String url = cla.url();
     if (url != null && url.length() > 0) {
       agreementGroup.setVisible(true);
       agreementHtml.setText(Gerrit.C.rpcStatusWorking());
@@ -250,7 +248,7 @@
       agreementGroup.setVisible(false);
     }
 
-    finalGroup.setVisible(cla.getAutoVerify() != null);
+    finalGroup.setVisible(cla.autoVerifyGroup() != null);
     yesIAgreeBox.setText("");
     submit.setEnabled(false);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
index c32a846..73557aa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
@@ -70,7 +70,7 @@
     formBody.add(contactGroup);
 
     if (Gerrit.getUserAccount().username() == null
-        && Gerrit.info().auth().canEdit(FieldName.USER_NAME)) {
+        && Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME)) {
       final FlowPanel fp = new FlowPanel();
       fp.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
       fp.add(new SmallHeading(Util.C.welcomeUsernameHeading()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
index f388436..d70121b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -86,7 +87,7 @@
   }
 
   private boolean canEditUserName() {
-    return Gerrit.info().auth().canEdit(Account.FieldName.USER_NAME);
+    return Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME);
   }
 
   private void confirmSetUserName() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
index a0f36b9..b4b4390 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.common.data.AccountSecurity;
-import com.google.gerrit.common.data.AccountService;
 import com.google.gerrit.common.data.ProjectAdminService;
 import com.google.gwt.core.client.GWT;
 import com.google.gwtjsonrpc.client.JsonUtil;
@@ -23,14 +22,10 @@
 public class Util {
   public static final AccountConstants C = GWT.create(AccountConstants.class);
   public static final AccountMessages M = GWT.create(AccountMessages.class);
-  public static final AccountService ACCOUNT_SVC;
   public static final AccountSecurity ACCOUNT_SEC;
   public static final ProjectAdminService PROJECT_SVC;
 
   static {
-    ACCOUNT_SVC = GWT.create(AccountService.class);
-    JsonUtil.bind(ACCOUNT_SVC, "rpc/AccountService");
-
     ACCOUNT_SEC = GWT.create(AccountSecurity.class);
     JsonUtil.bind(ACCOUNT_SEC, "rpc/AccountSecurity");
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
index 254d3e6..7a32f01 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
@@ -21,8 +21,8 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.groups.GroupApi;
 import com.google.gerrit.client.groups.GroupAuditEventInfo;
-import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.FancyFlexTable;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index a71dffe..22a57a4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
 import com.google.gerrit.client.ui.OnEditEnabler;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index 7c0c8f6..053e7e0c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -18,8 +18,8 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
index 8c00ba7..cbe8a06 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.client.Dispatcher.toGroup;
 
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.MenuScreen;
 import com.google.gerrit.reviewdb.client.AccountGroup;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 2fe5978..3290aac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -123,6 +123,9 @@
 	abandon, \
 	addPatchSet, \
 	create, \
+	createTag, \
+	createSignedTag, \
+	delete, \
 	deleteDrafts, \
 	editHashtags, \
 	editTopicName, \
@@ -133,8 +136,6 @@
 	publishDrafts, \
 	push, \
 	pushMerge, \
-	pushTag, \
-	pushSignedTag, \
 	read, \
 	rebase, \
 	removeReviewer, \
@@ -145,6 +146,9 @@
 abandon = Abandon
 addPatchSet = Add Patch Set
 create = Create Reference
+createTag = Create Annotated Tag
+createSignedTag = Create Signed Tag
+delete = Delete Reference
 deleteDrafts = Delete Drafts
 editHashtags = Edit Hashtags
 editTopicName = Edit Topic Name
@@ -155,8 +159,6 @@
 publishDrafts = Publish Drafts
 push = Push
 pushMerge = Push Merge Commit
-pushTag = Push Annotated Tag
-pushSignedTag = Push Signed Tag
 read = Read
 rebase = Rebase
 removeReviewer = Remove Reviewer
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
index a2ba5cd..4efaa61 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.client.NotFoundScreen;
 import com.google.gerrit.client.account.AccountCapabilities;
 import com.google.gerrit.client.groups.GroupApi;
-import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.Screen;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
index 64fc0e5..94d15bd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -18,9 +18,9 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.groups.GroupList;
 import com.google.gerrit.client.groups.GroupMap;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
index f66307c..be5bdcb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME;
 import static com.google.gerrit.common.data.Permission.PUSH;
-import static com.google.gerrit.common.data.Permission.PUSH_TAG;
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
@@ -143,7 +142,7 @@
     initWidget(uiBinder.createAndBindUi(this));
 
     String name = permission.getName();
-    boolean canForce = PUSH.equals(name) || PUSH_TAG.equals(name);
+    boolean canForce = PUSH.equals(name);
     if (canForce) {
       String ref = section.getName();
       canForce = !ref.startsWith("refs/for/") && !ref.startsWith("^refs/for/");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
index 62c3636..2a8dacf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
@@ -30,4 +30,5 @@
   String submittedTogether(int count);
   String submittedTogether(String count);
   String editPatchSet(int patchSet);
+  String failedToLoadFileList(String error);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
index 6461899..743945d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
@@ -6,3 +6,4 @@
 sameTopic = Same Topic ({0})
 submittedTogether = Submitted Together ({0})
 editPatchSet = edit:{0}
+failedToLoadFileList = Failed to load file list: {0}
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 b2e9f28..9a4a049 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
@@ -107,8 +107,13 @@
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 public class ChangeScreen extends Screen {
+  private static final Logger logger =
+      Logger.getLogger(ChangeScreen.class.getName());
+
   interface Binder extends UiBinder<HTMLPanel, ChangeScreen> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
@@ -282,13 +287,17 @@
             info.init();
             addExtensionPoints(info, initCurrentRevision(info));
 
-            RevisionInfo rev = info.revision(revision);
+            final RevisionInfo rev = info.revision(revision);
             CallbackGroup group = new CallbackGroup();
             loadCommit(rev, group);
 
             group.addListener(new GerritCallback<Void>() {
               @Override
               public void onSuccess(Void result) {
+                if (base == null && rev.commit().parents().length() > 1) {
+                  base = Gerrit.getUserPreferences()
+                      .defaultBaseForMerges().getBase();
+                }
                 loadConfigInfo(info, base);
               }
             });
@@ -569,35 +578,40 @@
   }
 
   private void initEditMode(ChangeInfo info, String revision) {
-    if (Gerrit.isSignedIn() && info.status().isOpen()) {
+    if (Gerrit.isSignedIn()) {
       RevisionInfo rev = info.revision(revision);
-      if (isEditModeEnabled(info, rev)) {
-        editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
-        addFile.setVisible(!editMode.isVisible());
-        deleteFile.setVisible(!editMode.isVisible());
-        renameFile.setVisible(!editMode.isVisible());
-        reviewMode.setVisible(!editMode.isVisible());
-        addFileAction = new AddFileAction(
-            changeId, info.revision(revision),
-            style, addFile, files);
-        deleteFileAction = new DeleteFileAction(
-            changeId, info.revision(revision),
-            style, addFile);
-        renameFileAction = new RenameFileAction(
-            changeId, info.revision(revision),
-            style, addFile);
-      } else {
-        editMode.setVisible(false);
-        addFile.setVisible(false);
-        reviewMode.setVisible(false);
-      }
-
-      if (rev.isEdit()) {
-        if (info.hasEditBasedOnCurrentPatchSet()) {
-          publishEdit.setVisible(true);
+      if (info.status().isOpen()) {
+        if (isEditModeEnabled(info, rev)) {
+          editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW);
+          addFile.setVisible(!editMode.isVisible());
+          deleteFile.setVisible(!editMode.isVisible());
+          renameFile.setVisible(!editMode.isVisible());
+          reviewMode.setVisible(!editMode.isVisible());
+          addFileAction = new AddFileAction(
+              changeId, info.revision(revision),
+              style, addFile, files);
+          deleteFileAction = new DeleteFileAction(
+              changeId, info.revision(revision),
+              style, addFile);
+          renameFileAction = new RenameFileAction(
+              changeId, info.revision(revision),
+              style, addFile);
         } else {
-          rebaseEdit.setVisible(true);
+          editMode.setVisible(false);
+          addFile.setVisible(false);
+          reviewMode.setVisible(false);
         }
+
+        if (rev.isEdit()) {
+          if (info.hasEditBasedOnCurrentPatchSet()) {
+            publishEdit.setVisible(true);
+          } else {
+            rebaseEdit.setVisible(true);
+          }
+          deleteEdit.setVisible(true);
+        }
+      } else if (rev.isEdit()) {
+        deleteEdit.setStyleName(style.highlight());
         deleteEdit.setVisible(true);
       }
     }
@@ -616,37 +630,39 @@
 
   @UiHandler("publishEdit")
   void onPublishEdit(@SuppressWarnings("unused") ClickEvent e) {
-    EditActions.publishEdit(changeId);
+    EditActions.publishEdit(changeId, publishEdit, rebaseEdit, deleteEdit);
   }
 
   @UiHandler("rebaseEdit")
   void onRebaseEdit(@SuppressWarnings("unused") ClickEvent e) {
-    EditActions.rebaseEdit(changeId);
+    EditActions.rebaseEdit(changeId, publishEdit, rebaseEdit, deleteEdit);
   }
 
   @UiHandler("deleteEdit")
   void onDeleteEdit(@SuppressWarnings("unused") ClickEvent e) {
     if (Window.confirm(Resources.C.deleteChangeEdit())) {
-      EditActions.deleteEdit(changeId);
+      EditActions.deleteEdit(changeId, publishEdit, rebaseEdit, deleteEdit);
     }
   }
 
   @UiHandler("publish")
   void onPublish(@SuppressWarnings("unused") ClickEvent e) {
-    DraftActions.publish(changeId, revision);
+    DraftActions.publish(changeId, revision, publish, deleteRevision,
+        deleteChange);
   }
 
   @UiHandler("deleteRevision")
   void onDeleteRevision(@SuppressWarnings("unused") ClickEvent e) {
     if (Window.confirm(Resources.C.deleteDraftRevision())) {
-      DraftActions.delete(changeId, revision);
+      DraftActions.delete(changeId, revision, publish, deleteRevision,
+          deleteChange);
     }
   }
 
   @UiHandler("deleteChange")
   void onDeleteChange(@SuppressWarnings("unused") ClickEvent e) {
     if (Window.confirm(Resources.C.deleteDraftChange())) {
-      DraftActions.delete(changeId);
+      DraftActions.delete(changeId, publish, deleteRevision, deleteChange);
     }
   }
 
@@ -918,7 +934,7 @@
   }
 
   private void loadConfigInfo(final ChangeInfo info, String base) {
-    RevisionInfo rev = info.revision(revision);
+    final RevisionInfo rev = info.revision(revision);
     RevisionInfo b = resolveRevisionOrPatchSetId(info, base, null);
 
     CallbackGroup group = new CallbackGroup();
@@ -933,16 +949,32 @@
     } else {
       loadDiff(b, rev, lastReply, group);
     }
+    group.addListener(new AsyncCallback<Void>() {
+      @Override
+      public void onSuccess(Void result) {
+        loadConfigInfo(info, rev);
+      }
 
+      @Override
+      public void onFailure(Throwable caught) {
+        logger.log(Level.SEVERE,
+            "Loading file list and inline comments failed: "
+                + caught.getMessage());
+        loadConfigInfo(info, rev);
+      }
+    });
+    group.done();
+  }
+
+  private void loadConfigInfo(final ChangeInfo info, RevisionInfo rev) {
     if (loaded) {
-      group.done();
       return;
     }
 
     RevisionInfoCache.add(changeId, rev);
     ConfigInfoCache.add(info);
     ConfigInfoCache.get(info.projectNameKey(),
-      group.addFinal(new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
+      new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
         @Override
         protected void preDisplay(Entry result) {
           loaded = true;
@@ -951,7 +983,7 @@
           renderChangeInfo(info);
           loadRevisionInfo();
         }
-      }));
+      });
   }
 
   static Timestamp myLastReply(ChangeInfo info) {
@@ -1012,6 +1044,7 @@
 
               @Override
               public void onFailure(Throwable caught) {
+                files.showError(caught);
               }
             }));
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
index 634190a2..6787576 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
@@ -21,23 +21,25 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
 
 public class DraftActions {
 
-  static void publish(Change.Id id, String revision) {
-    ChangeApi.publish(id.get(), revision, cs(id));
+  static void publish(Change.Id id, String revision, Button... draftButtons) {
+    ChangeApi.publish(id.get(), revision, cs(id, draftButtons));
   }
 
-  static void delete(Change.Id id, String revision) {
-    ChangeApi.deleteRevision(id.get(), revision, cs(id));
+  static void delete(Change.Id id, String revision, Button... draftButtons) {
+    ChangeApi.deleteRevision(id.get(), revision, cs(id, draftButtons));
   }
 
-  static void delete(Change.Id id) {
-    ChangeApi.deleteChange(id.get(), mine());
+  static void delete(Change.Id id, Button... draftButtons) {
+    ChangeApi.deleteChange(id.get(), mine(draftButtons));
   }
 
   public static GerritCallback<JavaScriptObject> cs(
-      final Change.Id id) {
+      final Change.Id id, final Button... draftButtons) {
+    setEnabled(false, draftButtons);
     return new GerritCallback<JavaScriptObject>() {
       @Override
       public void onSuccess(JavaScriptObject result) {
@@ -46,6 +48,7 @@
 
       @Override
       public void onFailure(Throwable err) {
+        setEnabled(true, draftButtons);
         if (SubmitFailureDialog.isConflict(err)) {
           new SubmitFailureDialog(err.getMessage()).center();
           Gerrit.display(PageLinks.toChange(id));
@@ -56,7 +59,9 @@
     };
   }
 
-  private static AsyncCallback<JavaScriptObject> mine() {
+  private static AsyncCallback<JavaScriptObject> mine(
+      final Button... draftButtons) {
+    setEnabled(false, draftButtons);
     return new GerritCallback<JavaScriptObject>() {
       @Override
       public void onSuccess(JavaScriptObject result) {
@@ -65,6 +70,7 @@
 
       @Override
       public void onFailure(Throwable err) {
+        setEnabled(true, draftButtons);
         if (SubmitFailureDialog.isConflict(err)) {
           new SubmitFailureDialog(err.getMessage()).center();
           Gerrit.display(PageLinks.MINE);
@@ -74,4 +80,12 @@
       }
     };
   }
+
+  private static void setEnabled(boolean enabled, Button... draftButtons) {
+    if (draftButtons != null) {
+      for (Button b : draftButtons) {
+        b.setEnabled(enabled);
+      }
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
index d11cf7e..97abddb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
@@ -20,23 +20,25 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.ui.Button;
 
 public class EditActions {
 
-  static void deleteEdit(Change.Id id) {
-    ChangeApi.deleteEdit(id.get(), cs(id));
+  static void deleteEdit(Change.Id id, Button... editButtons) {
+    ChangeApi.deleteEdit(id.get(), cs(id, editButtons));
   }
 
-  static void publishEdit(Change.Id id) {
-    ChangeApi.publishEdit(id.get(), cs(id));
+  static void publishEdit(Change.Id id, Button... editButtons) {
+    ChangeApi.publishEdit(id.get(), cs(id, editButtons));
   }
 
-  static void rebaseEdit(Change.Id id) {
-    ChangeApi.rebaseEdit(id.get(), cs(id));
+  static void rebaseEdit(Change.Id id, Button... editButtons) {
+    ChangeApi.rebaseEdit(id.get(), cs(id, editButtons));
   }
 
   public static GerritCallback<JavaScriptObject> cs(
-      final Change.Id id) {
+      final Change.Id id, final Button... editButtons) {
+    setEnabled(false, editButtons);
     return new GerritCallback<JavaScriptObject>() {
       @Override
       public void onSuccess(JavaScriptObject result) {
@@ -45,6 +47,7 @@
 
       @Override
       public void onFailure(Throwable err) {
+        setEnabled(true, editButtons);
         if (SubmitFailureDialog.isConflict(err)) {
           new SubmitFailureDialog(err.getMessage()).center();
           Gerrit.display(PageLinks.toChange(id));
@@ -54,4 +57,12 @@
       }
     };
   }
+
+  private static void setEnabled(boolean enabled, Button... editButtons) {
+    if (editButtons != null) {
+      for (Button b : editButtons) {
+        b.setEnabled(enabled);
+      }
+    }
+  }
 }
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 f0a7ce3..c0879e7 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
@@ -60,6 +60,7 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.ImageResourceRenderer;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
@@ -94,6 +95,7 @@
     String inserted();
     String deleted();
     String restoreDelete();
+    String error();
   }
 
   public enum Mode {
@@ -222,6 +224,13 @@
     }
   }
 
+  void showError(Throwable t) {
+    clear();
+    Label l = new Label(Resources.M.failedToLoadFileList(t.getMessage()));
+    add(l);
+    l.setStyleName(R.css().error());
+  }
+
   void markReviewed(JsArrayString reviewed) {
     if (table != null) {
       table.markReviewed(reviewed);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index cc5c9b7..2d868da 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -215,10 +215,11 @@
         EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
         new TabChangeListCallback(Tab.CHERRY_PICKS, info.project(), revision));
 
-    // TODO(sbeller): show only on latest revision
-    ChangeApi.change(info.legacyId().get()).view("submitted_together")
-        .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
-            info.project(), revision));
+    if (info.currentRevision().equals(revision)) {
+      ChangeApi.change(info.legacyId().get()).view("submitted_together")
+          .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER,
+              info.project(), revision));
+    }
 
     if (!Gerrit.info().change().isSubmitWholeTopicEnabled()
         && info.topic() != null && !"".equals(info.topic())) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
index a852fa0..2188c03 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.client.admin.Util;
 import com.google.gerrit.client.changes.ChangeApi;
-import com.google.gerrit.client.groups.GroupBaseInfo;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupBaseInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountSuggestOracle;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
index bde9755..6f514df 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -109,3 +109,7 @@
   white-space: nowrap;
 }
 
+.error {
+  color: #D33D3D;
+  font-weight: bold;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
index 93be87b..760f06d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.RestApi;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
index ed41b65..5bcdc6b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.groups;
 
 import com.google.gerrit.client.info.AccountInfo;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
index a24e1dc..f51ecb8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.groups;
 
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.core.client.JsArray;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
index 5532285..5e23049 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.groups;
 
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.user.client.rpc.AsyncCallback;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index a96624a..983d48c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.groups.GroupMap;
+import com.google.gerrit.client.info.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.reviewdb.client.AccountGroup;
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 943be7e..3d99883 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -82,7 +82,6 @@
       Modes.I.htmlmixed(),
       Modes.I.http(),
       Modes.I.idl(),
-      Modes.I.jade(),
       Modes.I.javascript(),
       Modes.I.jinja2(),
       Modes.I.jsx(),
@@ -110,6 +109,7 @@
       Modes.I.powershell(),
       Modes.I.properties(),
       Modes.I.protobuf(),
+      Modes.I.pug(),
       Modes.I.puppet(),
       Modes.I.python(),
       Modes.I.q(),
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 668a57f..218b96c 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -67,7 +67,6 @@
   @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();
@@ -95,6 +94,7 @@
   @Source("powershell.js") @DoNotEmbed DataResource powershell();
   @Source("properties.js") @DoNotEmbed DataResource properties();
   @Source("protobuf.js") @DoNotEmbed DataResource protobuf();
+  @Source("pug.js") @DoNotEmbed DataResource pug();
   @Source("puppet.js") @DoNotEmbed DataResource puppet();
   @Source("python.js") @DoNotEmbed DataResource python();
   @Source("q.js") @DoNotEmbed DataResource q();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
index 5146b31..7935bb6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd;
 
-import static com.google.gerrit.reviewdb.client.AuthType.OAUTH;
+import static com.google.gerrit.extensions.client.AuthType.OAUTH;
 
 import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
 import com.google.gerrit.server.config.AuthConfig;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 2c67182..71bb981 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.httpd.raw.CatServlet;
 import com.google.gerrit.httpd.raw.HostPageServlet;
 import com.google.gerrit.httpd.raw.LegacyGerritServlet;
@@ -30,7 +31,6 @@
 import com.google.gerrit.httpd.restapi.GroupsRestApiServlet;
 import com.google.gerrit.httpd.restapi.ProjectsRestApiServlet;
 import com.google.gerrit.httpd.rpc.doc.QueryDocumentationFilter;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AuthConfig;
@@ -64,6 +64,7 @@
 
     if (options.enableDefaultUi()) {
       filter("/").through(XsrfCookieFilter.class);
+      filter("/accounts/self/detail").through(XsrfCookieFilter.class);
       serve("/").with(HostPageServlet.class);
       serve("/Gerrit").with(LegacyGerritServlet.class);
       serve("/Gerrit/*").with(legacyGerritScreen());
@@ -97,6 +98,11 @@
 
     filter("/a/*").through(RequireIdentifiedUserFilter.class);
     serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class);
+
+    // Bind servlets for REST root collections.
+    // The '/plugins/' root collection is already handled by HttpPluginServlet
+    // which is bound in HttpPluginModule. We cannot bind it here again although
+    // this means that plugins can't add REST views on PLUGIN_KIND.
     serveRegex("^/(?:a/)?access/(.*)$").with(AccessRestApiServlet.class);
     serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
     serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 8594e30..27aff21 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -359,7 +359,6 @@
           if (Strings.isNullOrEmpty(entryTitle)) {
             entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' ');
           }
-          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
         } else {
           entryTitle = rsrc.substring(nameOffset).replace('-', ' ');
         }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 943d824..e3f3fb1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -15,6 +15,14 @@
 package com.google.gerrit.httpd.restapi;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD;
+import static com.google.common.net.HttpHeaders.ORIGIN;
+import static com.google.common.net.HttpHeaders.VARY;
 import static java.math.RoundingMode.CEILING;
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -35,9 +43,12 @@
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
+import com.google.common.base.Predicates;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
@@ -85,6 +96,7 @@
 import com.google.gerrit.server.OptionUtil;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.CapabilityUtils;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.util.http.RequestUtil;
 import com.google.gson.ExclusionStrategy;
 import com.google.gson.FieldAttributes;
@@ -103,6 +115,7 @@
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.TemporaryBuffer.Heap;
 import org.slf4j.Logger;
@@ -131,6 +144,7 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
 import java.util.zip.GZIPOutputStream;
 
 import javax.servlet.ServletException;
@@ -150,6 +164,9 @@
   // HTTP 422 Unprocessable Entity.
   // TODO: Remove when HttpServletResponse.SC_UNPROCESSABLE_ENTITY is available
   private static final int SC_UNPROCESSABLE_ENTITY = 422;
+  private static final String X_REQUESTED_WITH = "X-Requested-With";
+  private static final ImmutableSet<String> ALLOWED_CORS_REQUEST_HEADERS =
+      ImmutableSet.of(X_REQUESTED_WITH);
 
   private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
 
@@ -174,18 +191,29 @@
     final Provider<ParameterParser> paramParser;
     final AuditService auditService;
     final RestApiMetrics metrics;
+    final Pattern allowOrigin;
 
     @Inject
     Globals(Provider<CurrentUser> currentUser,
         DynamicItem<WebSession> webSession,
         Provider<ParameterParser> paramParser,
         AuditService auditService,
-        RestApiMetrics metrics) {
+        RestApiMetrics metrics,
+        @GerritServerConfig Config cfg) {
       this.currentUser = currentUser;
       this.webSession = webSession;
       this.paramParser = paramParser;
       this.auditService = auditService;
       this.metrics = metrics;
+      allowOrigin = makeAllowOrigin(cfg);
+    }
+
+    private static Pattern makeAllowOrigin(Config cfg) {
+      String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
+      if (allow.length > 0) {
+        return Pattern.compile(Joiner.on('|').join(allow));
+      }
+      return null;
     }
   }
 
@@ -222,6 +250,11 @@
     ViewData viewData = null;
 
     try {
+      if (isCorsPreflight(req)) {
+        doCorsPreflight(req, res);
+        return;
+      }
+      checkCors(req, res);
       checkUserSession(req);
 
       List<IdString> path = splitPath(req);
@@ -232,7 +265,7 @@
       viewData = new ViewData(null, null);
 
       if (path.isEmpty()) {
-        if (isGetOrHead(req)) {
+        if (isRead(req)) {
           viewData = new ViewData(null, rc.list());
         } else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
           @SuppressWarnings("unchecked")
@@ -273,7 +306,7 @@
             (RestCollection<RestResource, RestResource>) viewData.view;
 
         if (path.isEmpty()) {
-          if (isGetOrHead(req)) {
+          if (isRead(req)) {
             viewData = new ViewData(null, c.list());
           } else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
             @SuppressWarnings("unchecked")
@@ -330,7 +363,7 @@
         return;
       }
 
-      if (viewData.view instanceof RestReadView<?> && isGetOrHead(req)) {
+      if (viewData.view instanceof RestReadView<?> && isRead(req)) {
         result = ((RestReadView<RestResource>) viewData.view).apply(rsrc);
       } else if (viewData.view instanceof RestModifyView<?, ?>) {
         @SuppressWarnings("unchecked")
@@ -428,6 +461,72 @@
     }
   }
 
+  private void checkCors(HttpServletRequest req, HttpServletResponse res) {
+    String origin = req.getHeader(ORIGIN);
+    if (isRead(req)
+        && !Strings.isNullOrEmpty(origin)
+        && isOriginAllowed(origin)) {
+      res.addHeader(VARY, ORIGIN);
+      setCorsHeaders(res, origin);
+    }
+  }
+
+  private static boolean isCorsPreflight(HttpServletRequest req) {
+    return "OPTIONS".equals(req.getMethod())
+        && !Strings.isNullOrEmpty(req.getHeader(ORIGIN))
+        && !Strings.isNullOrEmpty(req.getHeader(ACCESS_CONTROL_REQUEST_METHOD));
+  }
+
+  private void doCorsPreflight(HttpServletRequest req,
+      HttpServletResponse res) throws BadRequestException {
+    CacheHeaders.setNotCacheable(res);
+    res.setHeader(VARY, Joiner.on(", ").join(ImmutableList.of(
+        ORIGIN,
+        ACCESS_CONTROL_REQUEST_METHOD)));
+
+    String origin = req.getHeader(ORIGIN);
+    if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) {
+      throw new BadRequestException("CORS not allowed");
+    }
+
+    String method = req.getHeader(ACCESS_CONTROL_REQUEST_METHOD);
+    if (!"GET".equals(method) && !"HEAD".equals(method)) {
+      throw new BadRequestException(method + " not allowed in CORS");
+    }
+
+    String headers = req.getHeader(ACCESS_CONTROL_REQUEST_HEADERS);
+    if (headers != null) {
+      res.addHeader(VARY, ACCESS_CONTROL_REQUEST_HEADERS);
+      String badHeader = Iterables.getFirst(
+          Iterables.filter(
+              Splitter.on(',').trimResults().split(headers),
+              Predicates.not(Predicates.in(ALLOWED_CORS_REQUEST_HEADERS))),
+          null);
+      if (badHeader != null) {
+        throw new BadRequestException(badHeader + " not allowed in CORS");
+      }
+    }
+
+    res.setStatus(SC_OK);
+    setCorsHeaders(res, origin);
+    res.setContentType("text/plain");
+    res.setContentLength(0);
+  }
+
+  private void setCorsHeaders(HttpServletResponse res, String origin) {
+    res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
+    res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
+    res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET, OPTIONS");
+    res.setHeader(
+        ACCESS_CONTROL_ALLOW_HEADERS,
+        Joiner.on(", ").join(ALLOWED_CORS_REQUEST_HEADERS));
+  }
+
+  private boolean isOriginAllowed(String origin) {
+    return globals.allowOrigin != null
+        && globals.allowOrigin.matcher(origin).matches();
+  }
+
   private static String messageOr(Throwable t, String defaultMessage) {
     if (!Strings.isNullOrEmpty(t.getMessage())) {
       return t.getMessage();
@@ -438,7 +537,7 @@
   @SuppressWarnings({"unchecked", "rawtypes"})
   private static boolean notModified(HttpServletRequest req, RestResource rsrc,
       RestView<RestResource> view) {
-    if (!isGetOrHead(req)) {
+    if (!isRead(req)) {
       return false;
     }
 
@@ -469,7 +568,7 @@
   private static <R extends RestResource> void configureCaching(
       HttpServletRequest req, HttpServletResponse res, R rsrc,
       RestView<R> view, CacheControl c) {
-    if (isGetOrHead(req)) {
+    if (isRead(req)) {
       switch (c.getType()) {
         case NONE:
         default:
@@ -972,25 +1071,20 @@
   private void checkUserSession(HttpServletRequest req)
       throws AuthException {
     CurrentUser user = globals.currentUser.get();
-    if (isStateChange(req)) {
-      if (user instanceof AnonymousUser) {
-        throw new AuthException("Authentication required");
-      } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
-        throw new AuthException("Invalid authentication method. In order to authenticate, "
-            + "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
-      }
+    if (isRead(req)) {
+      user.setAccessPath(AccessPath.REST_API);
+    } else if (user instanceof AnonymousUser) {
+      throw new AuthException("Authentication required");
+    } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
+      throw new AuthException("Invalid authentication method. In order to authenticate, "
+          + "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
     }
-    user.setAccessPath(AccessPath.REST_API);
   }
 
-  private static boolean isGetOrHead(HttpServletRequest req) {
+  private static boolean isRead(HttpServletRequest req) {
     return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod());
   }
 
-  private static boolean isStateChange(HttpServletRequest req) {
-    return !isGetOrHead(req);
-  }
-
   private void checkRequiresCapability(ViewData viewData) throws AuthException {
     CapabilityUtils.checkRequiresCapability(globals.currentUser,
         viewData.pluginName, viewData.view.getClass());
@@ -1029,7 +1123,7 @@
 
   static long replyText(@Nullable HttpServletRequest req,
       HttpServletResponse res, String text) throws IOException {
-    if ((req == null || isGetOrHead(req)) && isMaybeHTML(text)) {
+    if ((req == null || isRead(req)) && isMaybeHTML(text)) {
       return replyJson(req, res, ImmutableMultimap.of("pp", "0"), new JsonPrimitive(text));
     }
     if (!text.endsWith("\n")) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index c0fb86b..bda2d91 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -14,11 +14,8 @@
 
 package com.google.gerrit.httpd.rpc;
 
-import com.google.common.collect.Lists;
-import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.SshHostKey;
 import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -32,7 +29,6 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 
 import javax.servlet.http.HttpServletRequest;
@@ -45,28 +41,12 @@
 
   private final List<HostKey> hostKeys;
   private final Provider<HttpServletRequest> httpRequest;
-  private final ProjectCache projectCache;
 
   @Inject
   SystemInfoServiceImpl(SshInfo daemon,
-      Provider<HttpServletRequest> hsr,
-      ProjectCache pc) {
+      Provider<HttpServletRequest> hsr) {
     hostKeys = daemon.getHostKeys();
     httpRequest = hsr;
-    projectCache = pc;
-  }
-
-  @Override
-  public void contributorAgreements(
-      final AsyncCallback<List<ContributorAgreement>> callback) {
-    Collection<ContributorAgreement> agreements =
-        projectCache.getAllProjects().getConfig().getContributorAgreements();
-    List<ContributorAgreement> cas =
-        Lists.newArrayListWithCapacity(agreements.size());
-    for (ContributorAgreement ca : agreements) {
-      cas.add(ca.forUi());
-    }
-    callback.onSuccess(cas);
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
index 62778eb..d32fdaf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
@@ -28,12 +28,10 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        factory(AgreementInfoFactory.Factory.class);
         factory(DeleteExternalIds.Factory.class);
         factory(ExternalIdDetailFactory.Factory.class);
       }
     });
     rpc(AccountSecurityImpl.class);
-    rpc(AccountServiceImpl.class);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index 8fcf9ea..3d05548 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -14,74 +14,31 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
-import com.google.common.base.Strings;
-import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.common.data.AccountSecurity;
-import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.extensions.events.AgreementSignup;
-import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.io.IOException;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
 class AccountSecurityImpl extends BaseServiceImplementation implements
     AccountSecurity {
-  private final Realm realm;
-  private final ProjectCache projectCache;
-  private final Provider<IdentifiedUser> user;
-  private final AccountByEmailCache byEmailCache;
-  private final AccountCache accountCache;
-
   private final DeleteExternalIds.Factory deleteExternalIdsFactory;
   private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
 
-  private final GroupCache groupCache;
-  private final AuditService auditService;
-  private final AgreementSignup agreementSignup;
-
   @Inject
   AccountSecurityImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser,
-      final Realm r, final Provider<IdentifiedUser> u,
-      final ProjectCache pc,
-      final AccountByEmailCache abec, final AccountCache uac,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
-      final ExternalIdDetailFactory.Factory externalIdDetailFactory,
-      final GroupCache groupCache,
-      final AuditService auditService,
-      AgreementSignup agreementSignup) {
+      final ExternalIdDetailFactory.Factory externalIdDetailFactory) {
     super(schema, currentUser);
-    realm = r;
-    user = u;
-    projectCache = pc;
-    byEmailCache = abec;
-    accountCache = uac;
-    this.auditService = auditService;
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
     this.externalIdDetailFactory = externalIdDetailFactory;
-    this.groupCache = groupCache;
-    this.agreementSignup = agreementSignup;
   }
 
   @Override
@@ -94,84 +51,4 @@
       final AsyncCallback<Set<AccountExternalId.Key>> callback) {
     deleteExternalIdsFactory.create(keys).to(callback);
   }
-
-  @Override
-  public void updateContact(final String name, final String emailAddr,
-      final AsyncCallback<Account> callback) {
-    run(callback, new Action<Account>() {
-      @Override
-      public Account run(ReviewDb db)
-          throws OrmException, Failure, IOException {
-        IdentifiedUser self = user.get();
-        final Account me = db.accounts().get(self.getAccountId());
-        final String oldEmail = me.getPreferredEmail();
-        if (realm.allowsEdit(Account.FieldName.FULL_NAME)) {
-          me.setFullName(Strings.emptyToNull(name));
-        }
-        if (!Strings.isNullOrEmpty(emailAddr)
-            && !self.hasEmailAddress(emailAddr)) {
-          throw new Failure(new PermissionDeniedException("Email address must be verified"));
-        }
-        me.setPreferredEmail(Strings.emptyToNull(emailAddr));
-        db.accounts().update(Collections.singleton(me));
-        if (!eq(oldEmail, me.getPreferredEmail())) {
-          byEmailCache.evict(oldEmail);
-          byEmailCache.evict(me.getPreferredEmail());
-        }
-        accountCache.evict(me.getId());
-        return me;
-      }
-    });
-  }
-
-  private static boolean eq(final String a, final String b) {
-    if (a == null && b == null) {
-      return true;
-    }
-    return a != null && a.equals(b);
-  }
-
-  @Override
-  public void enterAgreement(final String agreementName,
-      final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      @Override
-      public VoidResult run(final ReviewDb db)
-          throws OrmException, Failure, IOException {
-        ContributorAgreement ca = projectCache.getAllProjects().getConfig()
-            .getContributorAgreement(agreementName);
-        if (ca == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        if (ca.getAutoVerify() == null) {
-          throw new Failure(new IllegalStateException(
-              "cannot enter a non-autoVerify agreement"));
-        } else if (ca.getAutoVerify().getUUID() == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        AccountGroup group = groupCache.get(ca.getAutoVerify().getUUID());
-        if (group == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        Account account = user.get().getAccount();
-        agreementSignup.fire(account, ca.getName());
-
-        final AccountGroupMember.Key key =
-            new AccountGroupMember.Key(account.getId(), group.getId());
-        AccountGroupMember m = db.accountGroupMembers().get(key);
-        if (m == null) {
-          m = new AccountGroupMember(key);
-          auditService.dispatchAddAccountsToGroup(account.getId(), Collections
-              .singleton(m));
-          db.accountGroupMembers().insert(Collections.singleton(m));
-          accountCache.evict(m.getAccountId());
-        }
-
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
deleted file mode 100644
index 8fba47d..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2008 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.rpc.account;
-
-import com.google.gerrit.common.data.AccountService;
-import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-class AccountServiceImpl extends BaseServiceImplementation implements
-    AccountService {
-  private final AgreementInfoFactory.Factory agreementInfoFactory;
-
-  @Inject
-  AccountServiceImpl(final Provider<ReviewDb> schema,
-      final Provider<IdentifiedUser> identifiedUser,
-      final AgreementInfoFactory.Factory agreementInfoFactory) {
-    super(schema, identifiedUser);
-    this.agreementInfoFactory = agreementInfoFactory;
-  }
-
-  @Override
-  public void myAgreements(final AsyncCallback<AgreementInfo> callback) {
-    agreementInfoFactory.create().to(callback);
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
deleted file mode 100644
index 91afd97..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2009 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.rpc.account;
-
-import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.common.data.ContributorAgreement;
-import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.PermissionRule.Action;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-class AgreementInfoFactory extends Handler<AgreementInfo> {
-  private static final Logger log = LoggerFactory.getLogger(AgreementInfoFactory.class);
-
-  interface Factory {
-    AgreementInfoFactory create();
-  }
-
-  private final IdentifiedUser user;
-  private final ProjectCache projectCache;
-
-  private AgreementInfo info;
-
-  @Inject
-  AgreementInfoFactory(final IdentifiedUser user,
-      final ProjectCache projectCache) {
-    this.user = user;
-    this.projectCache = projectCache;
-  }
-
-  @Override
-  public AgreementInfo call() throws Exception {
-    List<String> accepted = new ArrayList<>();
-    Map<String, ContributorAgreement> agreements = new HashMap<>();
-    Collection<ContributorAgreement> cas =
-        projectCache.getAllProjects().getConfig().getContributorAgreements();
-    for (ContributorAgreement ca : cas) {
-      agreements.put(ca.getName(), ca.forUi());
-
-      List<AccountGroup.UUID> groupIds = new ArrayList<>();
-      for (PermissionRule rule : ca.getAccepted()) {
-        if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)) {
-          if (rule.getGroup().getUUID() == null) {
-            log.warn("group \"" + rule.getGroup().getName() + "\" does not " +
-                " exist, referenced in CLA \"" + ca.getName() + "\"");
-          } else {
-            groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
-          }
-        }
-      }
-      if (user.getEffectiveGroups().containsAnyOf(groupIds)) {
-        accepted.add(ca.getName());
-      }
-    }
-
-    info = new AgreementInfo();
-    info.setAccepted(accepted);
-    info.setAgreements(agreements);
-    return info;
-  }
-}
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index 3a40252..791f9fd 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -24,12 +24,12 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.template.SiteHeaderFooter;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD
index 59b371a..ec86c9a 100644
--- a/gerrit-pgm/BUILD
+++ b/gerrit-pgm/BUILD
@@ -19,6 +19,7 @@
   '//lib/guice:guice-assistedinject',
   '//lib/guice:guice-servlet',
   '//lib/jgit/org.eclipse.jgit:jgit',
+  '//lib/joda:joda-time',
   '//lib/log:api',
   '//lib/log:log4j',
 ]
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 7ee3cde..d98f999 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -20,6 +20,7 @@
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.EventBroker;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.GerritOptions;
@@ -46,7 +47,6 @@
 import com.google.gerrit.pgm.util.LogFileCompressor;
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.change.ChangeCleanupRunner;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 9d27170..f5212ab 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -18,9 +18,9 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.config.ThreadSettingsConfig;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 2de71cc..136ec5a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Strings;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
@@ -27,7 +28,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index 6b30f80..a6471c7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -16,11 +16,11 @@
 
 import static com.google.gerrit.pgm.init.api.InitUtil.dnOf;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.pgm.init.api.ConsoleUI;
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
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 f16e2ec..978b87c 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
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 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.
@@ -99,21 +99,21 @@
     chmod(0755, site.gerrit_sh);
     chmod(0700, site.tmp_dir);
 
-    extractMailExample("Abandoned.vm");
-    extractMailExample("AddKey.vm");
-    extractMailExample("ChangeFooter.vm");
-    extractMailExample("ChangeSubject.vm");
-    extractMailExample("Comment.vm");
-    extractMailExample("CommentFooter.vm");
-    extractMailExample("DeleteReviewer.vm");
-    extractMailExample("DeleteVote.vm");
-    extractMailExample("Footer.vm");
-    extractMailExample("Merged.vm");
-    extractMailExample("NewChange.vm");
-    extractMailExample("RegisterNewEmail.vm");
-    extractMailExample("ReplacePatchSet.vm");
-    extractMailExample("Restored.vm");
-    extractMailExample("Reverted.vm");
+    extractMailExample("Abandoned.soy");
+    extractMailExample("AddKey.soy");
+    extractMailExample("ChangeFooter.soy");
+    extractMailExample("ChangeSubject.soy");
+    extractMailExample("Comment.soy");
+    extractMailExample("CommentFooter.soy");
+    extractMailExample("DeleteReviewer.soy");
+    extractMailExample("DeleteVote.soy");
+    extractMailExample("Footer.soy");
+    extractMailExample("Merged.soy");
+    extractMailExample("NewChange.soy");
+    extractMailExample("RegisterNewEmail.soy");
+    extractMailExample("ReplacePatchSet.soy");
+    extractMailExample("Restored.soy");
+    extractMailExample("Reverted.soy");
 
     if (!ui.isBatch()) {
       System.err.println();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
index 0360cd6..d39c2fd 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
-import com.google.gerrit.server.git.validators.CommitValidators;
 
 /** Module for batch programs that need git access. */
 public class BatchGitModule extends FactoryModule {
@@ -27,7 +26,6 @@
   protected void configure() {
     DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
     DynamicSet.setOf(binder(), CommitValidationListener.class);
-    factory(CommitValidators.Factory.class);
     install(new GitModule());
   }
 }
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK
index c18e497..7e38726 100644
--- a/gerrit-plugin-api/BUCK
+++ b/gerrit-plugin-api/BUCK
@@ -13,10 +13,20 @@
 
 java_binary(
   name = 'plugin-api',
+  merge_manifests = False,
+  manifest_file = ':manifest',
   deps = [':lib'],
   visibility = ['PUBLIC'],
 )
 
+genrule(
+  name = 'manifest',
+  cmd = 'echo "Manifest-Version: 1.0" >$OUT;' +
+    'echo "Implementation-Title: Gerrit Plugin API" >>$OUT;' +
+    'echo "Implementation-Vendor: Gerrit Code Review Project" >>$OUT',
+  out = 'manifest.txt',
+)
+
 java_library(
   name = 'lib',
   exported_deps = PLUGIN_API + [
@@ -32,20 +42,30 @@
     '//lib:gson',
     '//lib:guava',
     '//lib:gwtorm',
+    '//lib:icu4j',
     '//lib:jsch',
+    '//lib:jsr305',
     '//lib:mime-util',
+    '//lib:protobuf',
     '//lib:servlet-api-3_1',
+    '//lib:soy',
     '//lib:velocity',
     '//lib/commons:lang',
     '//lib/dropwizard:dropwizard-core',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
+    '//lib/guice:javax-inject',
+    '//lib/guice:multibindings',
     '//lib/guice:guice-servlet',
     '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet',
     '//lib/joda:joda-time',
     '//lib/log:api',
     '//lib/mina:sshd',
+    '//lib/ow2:ow2-asm',
+    '//lib/ow2:ow2-asm-analysis',
+    '//lib/ow2:ow2-asm-commons',
+    '//lib/ow2:ow2-asm-util',
     '//lib/prolog:compiler',
   ],
   visibility = ['PUBLIC'],
@@ -63,7 +83,7 @@
   name = 'plugin-api-javadoc',
   title = 'Gerrit Review Plugin API Documentation',
   pkgs = ['com.google.gerrit'],
-  paths = [n for n in SRCS],
+  source_jar = ':plugin-api-src',
   srcs = glob([n + '**/*.java' for n in SRCS]),
   deps = [
     ':plugin-api',
@@ -72,5 +92,4 @@
     '//lib/bouncycastle:bcpkix',
   ],
   visibility = ['PUBLIC'],
-  do_it_wrong = True,
 )
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD
index 2c18ca6..c761703 100644
--- a/gerrit-plugin-api/BUILD
+++ b/gerrit-plugin-api/BUILD
@@ -28,24 +28,33 @@
     '//gerrit-extension-api:api',
     '//gerrit-gwtexpui:server',
     '//gerrit-reviewdb:server',
-    '//lib:args4j',
-    '//lib:blame-cache',
-    '//lib/dropwizard:dropwizard-core',
-    '//lib:guava',
-    '//lib:gwtorm',
-    '//lib:jsch',
-    '//lib:mime-util',
-    '//lib:servlet-api-3_1',
-    '//lib:velocity',
     '//lib/commons:lang',
+    '//lib/dropwizard:dropwizard-core',
     '//lib/guice:guice',
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
-    '//lib/jgit/org.eclipse.jgit:jgit',
+    '//lib/guice:javax-inject',
+    '//lib/guice:multibindings',
     '//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet',
+    '//lib/jgit/org.eclipse.jgit:jgit',
     '//lib/joda:joda-time',
     '//lib/log:api',
     '//lib/mina:sshd',
+    '//lib/ow2:ow2-asm',
+    '//lib/ow2:ow2-asm-analysis',
+    '//lib/ow2:ow2-asm-commons',
+    '//lib/ow2:ow2-asm-util',
+    '//lib:args4j',
+    '//lib:blame-cache',
+    '//lib:guava',
+    '//lib:gwtorm',
+    '//lib:icu4j',
+    '//lib:jsch',
+    '//lib:mime-util',
+    '//lib:protobuf',
+    '//lib:servlet-api-3_1',
+    '//lib:soy',
+    '//lib:velocity',
   ],
   visibility = ['//visibility:public'],
 )
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 77bb111..9309921 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index d8e3583..e69da7c 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-archetype</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <name>Gerrit Code Review - Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Plugins</description>
   <url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
index 362b45c..5e9bc33 100644
--- a/gerrit-plugin-gwt-archetype/pom.xml
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwt-archetype</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <name>Gerrit Code Review - Web UI GWT Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI GWT Plugins</description>
   <url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-plugin-gwtui/BUCK b/gerrit-plugin-gwtui/BUCK
index 2ee0e19..575ebfc 100644
--- a/gerrit-plugin-gwtui/BUCK
+++ b/gerrit-plugin-gwtui/BUCK
@@ -1,8 +1,4 @@
-COMMON = ['gerrit-gwtui-common/src/main/java/']
-GWTEXPUI = ['gerrit-gwtexpui/src/main/java/']
-SRC = 'src/main/java/com/google/gerrit/'
-SRCS = glob([SRC + '**/*.java'])
-
+SRCS = glob(['src/main/java/com/google/gerrit/**/*.java'])
 DEPS = ['//lib/gwt:user']
 
 java_binary(
@@ -50,7 +46,7 @@
     'com.google.gwtexpui.safehtml',
     'com.google.gwtexpui.user',
   ],
-  paths = COMMON + GWTEXPUI,
+  source_jar = ':gwtui-api-src',
   srcs = SRCS,
   deps = DEPS + [
     '//lib:gwtjsonrpc',
@@ -61,5 +57,4 @@
     '//gerrit-reviewdb:client',
   ],
   visibility = ['PUBLIC'],
-  do_it_wrong = True,
 )
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 982d268..4b104c6 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-gwtui</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin GWT UI</name>
   <description>Common Classes for Gerrit GWT UI Plugins</description>
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
index e22478a..d31b455 100644
--- a/gerrit-plugin-js-archetype/pom.xml
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -20,7 +20,7 @@
 
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-js-archetype</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
   <description>Maven Archetype for Gerrit Web UI JavaScript Plugins</description>
   <url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index 9e36fc1..de2134b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -55,10 +55,6 @@
  * </ul>
  */
 public final class Account {
-  public enum FieldName {
-    FULL_NAME, USER_NAME, REGISTER_NEW_EMAIL
-  }
-
   public static final String USER_NAME_PATTERN_FIRST = "[a-zA-Z0-9]";
   public static final String USER_NAME_PATTERN_REST = "[a-zA-Z0-9._-]";
   public static final String USER_NAME_PATTERN_LAST = "[a-zA-Z0-9]";
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
index 41336791..5ae8847 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
 
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 4fc578c..080b52b 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -46,6 +46,7 @@
     '//lib:mime-util',
     '//lib:pegdown',
     '//lib:protobuf',
+    '//lib:soy',
     '//lib:tukaani-xz',
     '//lib:velocity',
     '//lib/antlr:java_runtime',
@@ -203,7 +204,6 @@
     '//lib:guava',
     '//lib:guava-retrying',
     '//lib:protobuf',
-    '//lib/commons:validator',
     '//lib/dropwizard:dropwizard-core',
     '//lib/guice:guice-assistedinject',
     '//lib/prolog:runtime',
diff --git a/gerrit-server/BUILD b/gerrit-server/BUILD
index 5a6b50f..a591fba 100644
--- a/gerrit-server/BUILD
+++ b/gerrit-server/BUILD
@@ -48,6 +48,7 @@
     '//lib:pegdown',
     '//lib:protobuf',
     '//lib:servlet-api-3_1',
+    '//lib:soy',
     '//lib:tukaani-xz',
     '//lib:velocity',
     '//lib/antlr:java_runtime',
@@ -188,6 +189,7 @@
     ['src/test/java/**/*.java'],
     exclude = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS
   ),
+  resources = glob(['src/test/resources/com/google/gerrit/server/mail/*']),
   deps = TESTUTIL_DEPS + [
     ':testutil',
     '//gerrit-antlr:query_exception',
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 bc2ec06..d69ad3f 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
@@ -77,22 +77,54 @@
     this.psUtil = psUtil;
   }
 
+  /**
+   * Apply approval copy settings from prior PatchSets to a new PatchSet.
+   *
+   * @param db review database.
+   * @param ctl change control for user uploading PatchSet
+   * @param ps new PatchSet
+   * @throws OrmException
+   */
   public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps)
       throws OrmException {
-    db.patchSetApprovals().insert(getForPatchSet(db, ctl, ps));
+    copy(db, ctl, ps, Collections.<PatchSetApproval>emptyList());
+  }
+
+  /**
+   * Apply approval copy settings from prior PatchSets to a new PatchSet.
+   *
+   * @param db review database.
+   * @param ctl change control for user uploading PatchSet
+   * @param ps new PatchSet
+   * @param dontCopy PatchSetApprovals indicating which (account, label) pairs
+   *        should not be copied
+   * @throws OrmException
+   */
+  public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps,
+      Iterable<PatchSetApproval> dontCopy) throws OrmException {
+    db.patchSetApprovals().insert(
+        getForPatchSet(db, ctl, ps, dontCopy));
   }
 
   Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
       ChangeControl ctl, PatchSet.Id psId) throws OrmException {
+    return getForPatchSet(db, ctl, psId,
+        Collections.<PatchSetApproval>emptyList());
+  }
+
+  Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
+      ChangeControl ctl, PatchSet.Id psId,
+      Iterable<PatchSetApproval> dontCopy) throws OrmException {
     PatchSet ps = psUtil.get(db, ctl.getNotes(), psId);
     if (ps == null) {
       return Collections.emptyList();
     }
-    return getForPatchSet(db, ctl, ps);
+    return getForPatchSet(db, ctl, ps, dontCopy);
   }
 
   private Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
-      ChangeControl ctl, PatchSet ps) throws OrmException {
+      ChangeControl ctl, PatchSet ps,
+      Iterable<PatchSetApproval> dontCopy) throws OrmException {
     checkNotNull(ps, "ps should not be null");
     ChangeData cd = changeDataFactory.create(db, ctl);
     try {
@@ -103,10 +135,16 @@
 
       Table<String, Account.Id, PatchSetApproval> wontCopy =
           HashBasedTable.create();
+      for (PatchSetApproval psa : dontCopy) {
+        wontCopy.put(psa.getLabel(), psa.getAccountId(), psa);
+      }
+
       Table<String, Account.Id, PatchSetApproval> byUser =
           HashBasedTable.create();
       for (PatchSetApproval psa : all.get(ps.getId())) {
-        byUser.put(psa.getLabel(), psa.getAccountId(), psa);
+        if (!wontCopy.contains(psa.getLabel(), psa.getAccountId())) {
+          byUser.put(psa.getLabel(), psa.getAccountId(), psa);
+        }
       }
 
       TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index e0526e4..5c0723a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -43,11 +43,15 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -73,6 +77,9 @@
  */
 @Singleton
 public class ApprovalsUtil {
+  private static final Logger log =
+      LoggerFactory.getLogger(ApprovalsUtil.class);
+
   private static final Ordering<PatchSetApproval> SORT_APPROVALS =
       Ordering.natural()
           .onResultOf(
@@ -99,13 +106,19 @@
   }
 
   private final NotesMigration migration;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final ApprovalCopier copier;
 
   @VisibleForTesting
   @Inject
   public ApprovalsUtil(NotesMigration migration,
+      IdentifiedUser.GenericFactory userFactory,
+      ChangeControl.GenericFactory changeControlFactory,
       ApprovalCopier copier) {
     this.migration = migration;
+    this.userFactory = userFactory;
+    this.changeControlFactory = changeControlFactory;
     this.copier = copier;
   }
 
@@ -164,8 +177,8 @@
       PatchSetInfo info, Iterable<Account.Id> wantReviewers,
       Collection<Account.Id> existingReviewers) throws OrmException {
     return addReviewers(db, update, labelTypes, change, ps.getId(),
-        ps.isDraft(), info.getAuthor().getAccount(),
-        info.getCommitter().getAccount(), wantReviewers, existingReviewers);
+        info.getAuthor().getAccount(), info.getCommitter().getAccount(),
+        wantReviewers, existingReviewers);
   }
 
   public List<PatchSetApproval> addReviewers(ReviewDb db, ChangeNotes notes,
@@ -189,12 +202,12 @@
         existingReviewers.add(entry.getKey());
       }
     }
-    return addReviewers(db, update, labelTypes, change, psId, false, null, null,
+    return addReviewers(db, update, labelTypes, change, psId, null, null,
         wantReviewers, existingReviewers);
   }
 
   private List<PatchSetApproval> addReviewers(ReviewDb db, ChangeUpdate update,
-      LabelTypes labelTypes, Change change, PatchSet.Id psId, boolean isDraft,
+      LabelTypes labelTypes, Change change, PatchSet.Id psId,
       Account.Id authorId, Account.Id committerId,
       Iterable<Account.Id> wantReviewers,
       Collection<Account.Id> existingReviewers) throws OrmException {
@@ -204,11 +217,11 @@
     }
 
     Set<Account.Id> need = Sets.newLinkedHashSet(wantReviewers);
-    if (authorId != null && !isDraft) {
+    if (authorId != null && canSee(db, update.getNotes(), authorId)) {
       need.add(authorId);
     }
 
-    if (committerId != null && !isDraft) {
+    if (committerId != null && canSee(db, update.getNotes(), committerId)) {
       need.add(committerId);
     }
     need.remove(change.getOwner());
@@ -229,6 +242,17 @@
     return Collections.unmodifiableList(cells);
   }
 
+  private boolean canSee(ReviewDb db, ChangeNotes notes, Account.Id accountId) {
+    try {
+      IdentifiedUser user = userFactory.create(accountId);
+      return changeControlFactory.controlFor(notes, user).isVisible(db);
+    } catch (OrmException | NoSuchChangeException e) {
+      log.warn(String.format("Failed to check if account %d can see change %d",
+          accountId.get(), notes.getChangeId().get()), e);
+      return false;
+    }
+  }
+
   /**
    * Adds accounts to a change as reviewers in the CC state.
    *
@@ -254,25 +278,40 @@
     return need;
   }
 
-  public void addApprovals(ReviewDb db, ChangeUpdate update,
+  /**
+   * Adds approvals to ChangeUpdate and writes to ReviewDb.
+   *
+   * @param db review database.
+   * @param update change update.
+   * @param labelTypes label types for the containing project.
+   * @param ps patch set being approved.
+   * @param changeCtl change control for user adding approvals.
+   * @param approvals approvals to add.
+   * @throws OrmException
+   */
+  public Iterable<PatchSetApproval> addApprovals(ReviewDb db, ChangeUpdate update,
       LabelTypes labelTypes, PatchSet ps, ChangeControl changeCtl,
       Map<String, Short> approvals) throws OrmException {
-    if (!approvals.isEmpty()) {
-      checkApprovals(approvals, changeCtl);
-      List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
-      Date ts = update.getWhen();
-      for (Map.Entry<String, Short> vote : approvals.entrySet()) {
-        LabelType lt = labelTypes.byLabel(vote.getKey());
-        cells.add(new PatchSetApproval(new PatchSetApproval.Key(
-            ps.getId(),
-            ps.getUploader(),
-            lt.getLabelId()),
-            vote.getValue(),
-            ts));
-        update.putApproval(vote.getKey(), vote.getValue());
-      }
-      db.patchSetApprovals().insert(cells);
+    if (approvals.isEmpty()) {
+      return Collections.emptyList();
     }
+    checkApprovals(approvals, changeCtl);
+    List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
+    Date ts = update.getWhen();
+    for (Map.Entry<String, Short> vote : approvals.entrySet()) {
+      LabelType lt = labelTypes.byLabel(vote.getKey());
+      cells.add(new PatchSetApproval(new PatchSetApproval.Key(
+          ps.getId(),
+          ps.getUploader(),
+          lt.getLabelId()),
+          vote.getValue(),
+          ts));
+    }
+    for (PatchSetApproval psa : cells) {
+      update.putApproval(psa.getLabel(), psa.getValue());
+    }
+    db.patchSetApprovals().insert(cells);
+    return cells;
   }
 
   public static void checkLabel(LabelTypes labelTypes, String name, Short value) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
index 30420e0..a0c6118 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -16,8 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.mail.EmailSender;
@@ -37,11 +36,11 @@
   }
 
   @Override
-  public Set<FieldName> getEditableFields() {
-    Set<Account.FieldName> fields = new  HashSet<>();
-    for (Account.FieldName n : Account.FieldName.values()) {
+  public Set<AccountFieldName> getEditableFields() {
+    Set<AccountFieldName> fields = new  HashSet<>();
+    for (AccountFieldName n : AccountFieldName.values()) {
       if (allowsEdit(n)) {
-        if (n == Account.FieldName.REGISTER_NEW_EMAIL) {
+        if (n == AccountFieldName.REGISTER_NEW_EMAIL) {
           if (emailSender != null && emailSender.isEnabled()) {
             fields.add(n);
           }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 19f9fcf..178cc79 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -201,14 +202,14 @@
       db.accountExternalIds().update(Collections.singleton(extId));
     }
 
-    if (!realm.allowsEdit(Account.FieldName.FULL_NAME)
+    if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
         && !Strings.isNullOrEmpty(who.getDisplayName())
         && !eq(user.getAccount().getFullName(), who.getDisplayName())) {
       toUpdate = load(toUpdate, user.getAccountId(), db);
       toUpdate.setFullName(who.getDisplayName());
     }
 
-    if (!realm.allowsEdit(Account.FieldName.USER_NAME)
+    if (!realm.allowsEdit(AccountFieldName.USER_NAME)
         && who.getUserName() != null
         && !eq(user.getUserName(), who.getUserName())) {
       log.warn(String.format("Not changing already set username %s to %s",
@@ -340,7 +341,7 @@
     } else {
       log.error(errorMessage);
     }
-    if (!realm.allowsEdit(Account.FieldName.USER_NAME)) {
+    if (!realm.allowsEdit(AccountFieldName.USER_NAME)) {
       // setting the given user name has failed, but the realm does not
       // allow the user to manually set a user name,
       // this means we would end with an account without user name
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index c985859..a864dab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.gerrit.extensions.client.AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT;
+
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -23,8 +26,6 @@
 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.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GetEmails.EmailInfo;
@@ -50,11 +51,11 @@
 
   private final Provider<CurrentUser> self;
   private final Realm realm;
-  private final AuthConfig authConfig;
   private final AccountManager accountManager;
   private final RegisterNewEmailSender.Factory registerNewEmailFactory;
   private final PutPreferred putPreferred;
   private final String email;
+  private final boolean isDevMode;
 
   @Inject
   CreateEmail(Provider<CurrentUser> self,
@@ -66,11 +67,11 @@
       @Assisted String email) {
     this.self = self;
     this.realm = realm;
-    this.authConfig = authConfig;
     this.accountManager = accountManager;
     this.registerNewEmailFactory = registerNewEmailFactory;
     this.putPreferred = putPreferred;
     this.email = email;
+    this.isDevMode = authConfig.getAuthType() == DEVELOPMENT_BECOME_ANY_ACCOUNT;
   }
 
   @Override
@@ -96,7 +97,7 @@
       throw new AuthException("not allowed to use no_confirmation");
     }
 
-    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+    if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow adding emails");
     }
 
@@ -113,8 +114,10 @@
 
     EmailInfo info = new EmailInfo();
     info.email = email;
-    if (input.noConfirmation
-        || authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+    if (input.noConfirmation || isDevMode) {
+      if (isDevMode) {
+        log.warn("skipping email validation in developer mode");
+      }
       try {
         accountManager.link(user.getAccountId(),
             AuthRequest.forEmail(email));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index eb3c9a0..57af333 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -15,8 +15,9 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -39,7 +40,7 @@
   }
 
   @Override
-  public boolean allowsEdit(final Account.FieldName field) {
+  public boolean allowsEdit(final AccountFieldName field) {
     if (authConfig.getAuthType() == AuthType.HTTP) {
       switch (field) {
         case USER_NAME:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
index f6c48af..94c099e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -16,11 +16,14 @@
 
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.DeleteActive.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -38,22 +41,28 @@
 
   private final Provider<ReviewDb> dbProvider;
   private final AccountCache byIdCache;
+  private final Provider<IdentifiedUser> self;
 
   @Inject
-  DeleteActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache) {
+  DeleteActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache,
+      Provider<IdentifiedUser> self) {
     this.dbProvider = dbProvider;
     this.byIdCache = byIdCache;
+    this.self = self;
   }
 
   @Override
   public Response<?> apply(AccountResource rsrc, Input input)
-      throws ResourceNotFoundException, OrmException, IOException {
+      throws RestApiException, OrmException, IOException {
     Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
     if (a == null) {
       throw new ResourceNotFoundException("account not found");
     }
     if (!a.isActive()) {
-      throw new ResourceNotFoundException();
+      throw new ResourceConflictException("account not active");
+    }
+    if (self.get() == rsrc.getUser()) {
+      throw new ResourceConflictException("cannot deactivate own account");
     }
     a.setActive(false);
     dbProvider.get().accounts().update(Collections.singleton(a));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index 76f63b7..1f073ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 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.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
@@ -67,7 +67,7 @@
   public Response<?> apply(IdentifiedUser user, String email)
       throws ResourceNotFoundException, ResourceConflictException,
       MethodNotAllowedException, OrmException, IOException {
-    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+    if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow deleting emails");
     }
     AccountExternalId.Key key = new AccountExternalId.Key(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
index d3b938f..a53f64e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
 
 /** Fake implementation of {@link Realm} that does not communicate. */
 public class FakeRealm extends AbstractRealm {
   @Override
-  public boolean allowsEdit(FieldName field) {
+  public boolean allowsEdit(AccountFieldName field) {
     return false;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
index 10b6df9..9864b45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.inject.Singleton;
@@ -22,9 +21,9 @@
 @Singleton
 public class GetActive implements RestReadView<AccountResource> {
   @Override
-  public Object apply(AccountResource rsrc) {
+  public Response<String> apply(AccountResource rsrc) {
     if (rsrc.getUser().getAccount().isActive()) {
-      return BinaryResult.create("ok\n");
+      return Response.ok("ok");
     }
     return Response.none();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java
index 9e1201a..46d6f11 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AgreementJson;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
@@ -46,14 +47,17 @@
 
   private final Provider<CurrentUser> self;
   private final ProjectCache projectCache;
+  private final AgreementJson agreementJson;
   private final boolean agreementsEnabled;
 
   @Inject
   GetAgreements(Provider<CurrentUser> self,
       ProjectCache projectCache,
+      AgreementJson agreementJson,
       @GerritServerConfig Config config) {
     this.self = self;
     this.projectCache = projectCache;
+    this.agreementJson = agreementJson;
     this.agreementsEnabled =
         config.getBoolean("auth", "contributorAgreements", false);
   }
@@ -85,17 +89,13 @@
             groupIds.add(rule.getGroup().getUUID());
           } else {
             log.warn("group \"" + rule.getGroup().getName() + "\" does not " +
-                " exist, referenced in CLA \"" + ca.getName() + "\"");
+                "exist, referenced in CLA \"" + ca.getName() + "\"");
           }
         }
       }
 
       if (user.getEffectiveGroups().containsAnyOf(groupIds)) {
-        AgreementInfo info = new AgreementInfo();
-        info.name = ca.getName();
-        info.description = ca.getDescription();
-        info.url = ca.getAgreementUrl();
-        results.add(info);
+        results.add(agreementJson.format(ca));
       }
     }
     return results;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
index 9197011..239b954 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.extensions.api.accounts.AccountInput;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.inject.Singleton;
 
@@ -23,7 +25,7 @@
 public class PutAccount
     implements RestModifyView<AccountResource, AccountInput> {
   @Override
-  public Object apply(AccountResource resource, AccountInput input)
+  public Response<AccountInfo> apply(AccountResource resource, AccountInput input)
       throws ResourceConflictException {
     throw new ResourceConflictException("account exists");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java
index 2fdf666..b8b902f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
@@ -68,7 +69,7 @@
   }
 
   @Override
-  public Object apply(AccountResource resource, AgreementInput input)
+  public Response<String> apply(AccountResource resource, AgreementInput input)
       throws IOException, OrmException, RestApiException {
     if (!agreementsEnabled) {
       throw new MethodNotAllowedException("contributor agreements disabled");
@@ -103,7 +104,7 @@
     addMembers.addMembers(group.getId(), ImmutableList.of(account.getId()));
     agreementSignup.fire(account, agreementName);
 
-    return agreementName;
+    return Response.ok(agreementName);
   }
 
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index e0b69a6..74c07e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.base.Strings;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -22,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -74,7 +74,7 @@
       input = new Input();
     }
 
-    if (!realm.allowsEdit(FieldName.FULL_NAME)) {
+    if (!realm.allowsEdit(AccountFieldName.FULL_NAME)) {
       throw new MethodNotAllowedException("realm does not allow editing name");
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
index e9dc393..29168ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.PutUsername.Input;
@@ -64,7 +64,7 @@
       throw new AuthException("not allowed to set username");
     }
 
-    if (!realm.allowsEdit(Account.FieldName.USER_NAME)) {
+    if (!realm.allowsEdit(AccountFieldName.USER_NAME)) {
       throw new MethodNotAllowedException("realm does not allow editing username");
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index 85fde4e..627f529 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 
@@ -21,10 +22,10 @@
 
 public interface Realm {
   /** Can the end-user modify this field of their own account? */
-  boolean allowsEdit(Account.FieldName field);
+  boolean allowsEdit(AccountFieldName field);
 
   /** Returns the account fields that the end-user can modify. */
-  Set<Account.FieldName> getEditableFields();
+  Set<AccountFieldName> getEditableFields();
 
   AuthRequest authenticate(AuthRequest who) throws AccountException;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 2af9f1d..3533fe8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.api.accounts;
 
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
 import com.google.gerrit.common.RawInputUtil;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.accounts.AccountApi;
@@ -31,6 +33,7 @@
 import com.google.gerrit.extensions.common.GpgKeyInfo;
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.server.GpgException;
@@ -38,8 +41,10 @@
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.AddSshKey;
 import com.google.gerrit.server.account.CreateEmail;
+import com.google.gerrit.server.account.DeleteActive;
 import com.google.gerrit.server.account.DeleteSshKey;
 import com.google.gerrit.server.account.DeleteWatchedProjects;
+import com.google.gerrit.server.account.GetActive;
 import com.google.gerrit.server.account.GetAgreements;
 import com.google.gerrit.server.account.GetAvatar;
 import com.google.gerrit.server.account.GetDiffPreferences;
@@ -48,6 +53,7 @@
 import com.google.gerrit.server.account.GetSshKeys;
 import com.google.gerrit.server.account.GetWatchedProjects;
 import com.google.gerrit.server.account.PostWatchedProjects;
+import com.google.gerrit.server.account.PutActive;
 import com.google.gerrit.server.account.PutAgreement;
 import com.google.gerrit.server.account.SetDiffPreferences;
 import com.google.gerrit.server.account.SetEditPreferences;
@@ -99,6 +105,9 @@
   private final SshKeys sshKeys;
   private final GetAgreements getAgreements;
   private final PutAgreement putAgreement;
+  private final GetActive getActive;
+  private final PutActive putActive;
+  private final DeleteActive deleteActive;
 
   @Inject
   AccountApiImpl(AccountLoader.Factory ailf,
@@ -126,6 +135,9 @@
       SshKeys sshKeys,
       GetAgreements getAgreements,
       PutAgreement putAgreement,
+      GetActive getActive,
+      PutActive putActive,
+      DeleteActive deleteActive,
       @Assisted AccountResource account) {
     this.account = account;
     this.accountLoaderFactory = ailf;
@@ -153,6 +165,9 @@
     this.gpgApiAdapter = gpgApiAdapter;
     this.getAgreements = getAgreements;
     this.putAgreement = putAgreement;
+    this.getActive = getActive;
+    this.putActive = putActive;
+    this.deleteActive = deleteActive;
   }
 
   @Override
@@ -169,6 +184,25 @@
   }
 
   @Override
+  public boolean getActive() throws RestApiException {
+    Response<String> result = getActive.apply(account);
+    return result.statusCode() == SC_OK && result.value().equals("ok");
+  }
+
+  @Override
+  public void setActive(boolean active) throws RestApiException {
+    try {
+      if (active) {
+        putActive.apply(account, new PutActive.Input());
+      } else {
+        deleteActive.apply(account, new DeleteActive.Input());
+      }
+    } catch (OrmException | IOException e) {
+      throw new RestApiException("Cannot set active", e);
+    }
+  }
+
+  @Override
   public String getAvatarUrl(int size) throws RestApiException {
     getAvatar.setSize(size);
     return getAvatar.apply(account).location();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
index a18c575..afda5fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.api.changes;
 
+import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.api.changes.DeleteVoteInput;
 import com.google.gerrit.extensions.api.changes.ReviewerApi;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -79,8 +80,13 @@
 
   @Override
   public void remove() throws RestApiException {
+    remove(new DeleteReviewerInput());
+  }
+
+  @Override
+  public void remove(DeleteReviewerInput input) throws RestApiException {
     try {
-      deleteReviewer.apply(reviewer, null);
+      deleteReviewer.apply(reviewer, input);
     } catch (UpdateException e) {
       throw new RestApiException("Cannot remove reviewer", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 6b5e83c..2c99318 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ActionInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.FileInfo;
 import com.google.gerrit.extensions.common.MergeableInfo;
 import com.google.gerrit.extensions.common.TestSubmitRuleInput;
@@ -44,6 +45,7 @@
 import com.google.gerrit.server.change.DraftComments;
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.change.Files;
+import com.google.gerrit.server.change.GetMergeList;
 import com.google.gerrit.server.change.GetPatch;
 import com.google.gerrit.server.change.GetRevisionActions;
 import com.google.gerrit.server.change.ListRevisionComments;
@@ -59,9 +61,11 @@
 import com.google.gerrit.server.change.TestSubmitType;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 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.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.Repository;
@@ -104,6 +108,7 @@
   private final GetRevisionActions revisionActions;
   private final TestSubmitType testSubmitType;
   private final TestSubmitType.Get getSubmitType;
+  private final Provider<GetMergeList> getMergeList;
 
   @Inject
   RevisionApiImpl(GitRepositoryManager repoManager,
@@ -132,6 +137,7 @@
       GetRevisionActions revisionActions,
       TestSubmitType testSubmitType,
       TestSubmitType.Get getSubmitType,
+      Provider<GetMergeList> getMergeList,
       @Assisted RevisionResource r) {
     this.repoManager = repoManager;
     this.changes = changes;
@@ -159,6 +165,7 @@
     this.revisionActions = revisionActions;
     this.testSubmitType = testSubmitType;
     this.getSubmitType = getSubmitType;
+    this.getMergeList = getMergeList;
     this.revision = r;
   }
 
@@ -264,7 +271,7 @@
       return ImmutableSet.copyOf((Iterable<String>) listFiles
           .setReviewed(true)
           .apply(revision).value());
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | PatchListNotAvailableException e) {
       throw new RestApiException("Cannot list reviewed files", e);
     }
   }
@@ -293,7 +300,7 @@
   public Map<String, FileInfo> files() throws RestApiException {
     try {
       return (Map<String, FileInfo>)listFiles.apply(revision).value();
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | PatchListNotAvailableException e) {
       throw new RestApiException("Cannot retrieve files", e);
     }
   }
@@ -304,7 +311,7 @@
     try {
       return (Map<String, FileInfo>) listFiles.setBase(base)
           .apply(revision).value();
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | PatchListNotAvailableException e) {
       throw new RestApiException("Cannot retrieve files", e);
     }
   }
@@ -315,7 +322,7 @@
     try {
       return (Map<String, FileInfo>) listFiles.setParent(parentNum)
           .apply(revision).value();
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | PatchListNotAvailableException e) {
       throw new RestApiException("Cannot retrieve files", e);
     }
   }
@@ -427,4 +434,21 @@
       throw new RestApiException("Cannot test submit type", e);
     }
   }
+
+  @Override
+  public MergeListRequest getMergeList() throws RestApiException {
+    return new MergeListRequest() {
+      @Override
+      public List<CommitInfo> get() throws RestApiException {
+        try {
+          GetMergeList gml = getMergeList.get();
+          gml.setUninterestingParent(getUninterestingParent());
+          gml.setAddLinks(getAddLinks());
+          return gml.apply(revision).value();
+        } catch (IOException e) {
+          throw new RestApiException("Cannot get merge list", e);
+        }
+      }
+    };
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
index 8339ecf..f433d2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -18,10 +18,12 @@
 import com.google.gerrit.extensions.api.config.Server;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.common.ServerInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.GetDiffPreferences;
 import com.google.gerrit.server.config.GetPreferences;
+import com.google.gerrit.server.config.GetServerInfo;
 import com.google.gerrit.server.config.SetDiffPreferences;
 import com.google.gerrit.server.config.SetPreferences;
 import com.google.inject.Inject;
@@ -37,16 +39,19 @@
   private final SetPreferences setPreferences;
   private final GetDiffPreferences getDiffPreferences;
   private final SetDiffPreferences setDiffPreferences;
+  private final GetServerInfo getServerInfo;
 
   @Inject
   ServerImpl(GetPreferences getPreferences,
       SetPreferences setPreferences,
       GetDiffPreferences getDiffPreferences,
-      SetDiffPreferences setDiffPreferences) {
+      SetDiffPreferences setDiffPreferences,
+      GetServerInfo getServerInfo) {
     this.getPreferences = getPreferences;
     this.setPreferences = setPreferences;
     this.getDiffPreferences = getDiffPreferences;
     this.setDiffPreferences = setDiffPreferences;
+    this.getServerInfo = getServerInfo;
   }
 
   @Override
@@ -55,6 +60,15 @@
   }
 
   @Override
+  public ServerInfo getInfo() throws RestApiException {
+    try {
+      return getServerInfo.apply(new ConfigResource());
+    } catch (IOException e) {
+      throw new RestApiException("Cannot get server info", e);
+    }
+  }
+
+  @Override
   public GeneralPreferencesInfo getDefaultPreferences()
       throws RestApiException {
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 7f5f2d2..4cb96b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.args4j;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
index 8dc7177..3dddf4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.auth.ldap;
 
-import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.auth.AuthBackend;
 import com.google.gerrit.server.auth.AuthException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 30b08a6..603efe0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -21,10 +21,11 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.extensions.client.AccountFieldName;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AbstractRealm;
 import com.google.gerrit.server.account.AccountException;
@@ -67,7 +68,7 @@
   private final AuthConfig authConfig;
   private final EmailExpander emailExpander;
   private final LoadingCache<String, Optional<Account.Id>> usernameCache;
-  private final Set<Account.FieldName> readOnlyAccountFields;
+  private final Set<AccountFieldName> readOnlyAccountFields;
   private final boolean fetchMemberOfEagerly;
   private final Config config;
 
@@ -91,13 +92,13 @@
     this.readOnlyAccountFields = new HashSet<>();
 
     if (optdef(config, "accountFullName", "DEFAULT") != null) {
-      readOnlyAccountFields.add(Account.FieldName.FULL_NAME);
+      readOnlyAccountFields.add(AccountFieldName.FULL_NAME);
     }
     if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
-      readOnlyAccountFields.add(Account.FieldName.USER_NAME);
+      readOnlyAccountFields.add(AccountFieldName.USER_NAME);
     }
     if (!authConfig.isAllowRegisterNewEmail()) {
-      readOnlyAccountFields.add(Account.FieldName.REGISTER_NEW_EMAIL);
+      readOnlyAccountFields.add(AccountFieldName.REGISTER_NEW_EMAIL);
     }
 
     fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true);
@@ -196,7 +197,7 @@
   }
 
   @Override
-  public boolean allowsEdit(final Account.FieldName field) {
+  public boolean allowsEdit(final AccountFieldName field) {
     return !readOnlyAccountFields.contains(field);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
index cf9000d..94a3ac2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
@@ -17,9 +17,9 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
 import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.extensions.client.AccountFieldName;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.server.account.AbstractRealm;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
@@ -37,7 +37,7 @@
 @Singleton
 public class OAuthRealm extends AbstractRealm {
   private final DynamicMap<OAuthLoginProvider> loginProviders;
-  private final Set<FieldName> editableAccountFields;
+  private final Set<AccountFieldName> editableAccountFields;
 
   @Inject
   OAuthRealm(DynamicMap<OAuthLoginProvider> loginProviders,
@@ -45,15 +45,15 @@
     this.loginProviders = loginProviders;
     this.editableAccountFields = new HashSet<>();
     if (config.getBoolean("oauth", null, "allowEditFullName", false)) {
-      editableAccountFields.add(FieldName.FULL_NAME);
+      editableAccountFields.add(AccountFieldName.FULL_NAME);
     }
     if (config.getBoolean("oauth", null, "allowRegisterNewEmail", false)) {
-      editableAccountFields.add(FieldName.REGISTER_NEW_EMAIL);
+      editableAccountFields.add(AccountFieldName.REGISTER_NEW_EMAIL);
     }
   }
 
   @Override
-  public boolean allowsEdit(FieldName field) {
+  public boolean allowsEdit(AccountFieldName field) {
     return editableAccountFields.contains(field);
   }
 
@@ -105,12 +105,12 @@
     }
     if (!Strings.isNullOrEmpty(userInfo.getEmailAddress())
         && (Strings.isNullOrEmpty(who.getUserName())
-            || !allowsEdit(FieldName.REGISTER_NEW_EMAIL))) {
+            || !allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL))) {
       who.setEmailAddress(userInfo.getEmailAddress());
     }
     if (!Strings.isNullOrEmpty(userInfo.getDisplayName())
         && (Strings.isNullOrEmpty(who.getDisplayName())
-            || !allowsEdit(FieldName.FULL_NAME))) {
+            || !allowsEdit(AccountFieldName.FULL_NAME))) {
       who.setDisplayName(userInfo.getDisplayName());
     }
     return who;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index adbcf22..c4bd68d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
@@ -50,6 +51,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Collection;
+
 @Singleton
 public class Abandon implements RestModifyView<ChangeResource, AbandonInput>,
     UiAction<ChangeResource> {
@@ -91,6 +94,11 @@
     return json.create(ChangeJson.NO_OPTIONS).format(change);
   }
 
+  public Change abandon(ChangeControl control)
+      throws RestApiException, UpdateException {
+    return abandon(control, "", NotifyHandling.ALL);
+  }
+
   public Change abandon(ChangeControl control, String msgTxt)
       throws RestApiException, UpdateException {
     return abandon(control, msgTxt, NotifyHandling.ALL);
@@ -98,31 +106,76 @@
 
   public Change abandon(ChangeControl control, String msgTxt,
       NotifyHandling notifyHandling) throws RestApiException, UpdateException {
-    CurrentUser user = control.getUser();
-    Account account = user.isIdentifiedUser()
-        ? user.asIdentifiedUser().getAccount()
-        : null;
-    Op op = new Op(msgTxt, account, notifyHandling);
-    try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
-        control.getProject().getNameKey(), user, TimeUtil.nowTs())) {
+    Op op = new Op(control.getUser(), msgTxt, notifyHandling);
+    try (BatchUpdate u =
+        batchUpdateFactory.create(
+            dbProvider.get(),
+            control.getProject().getNameKey(),
+            control.getUser(),
+            TimeUtil.nowTs())) {
       u.addOp(control.getId(), op).execute();
     }
     return op.change;
   }
 
+  /**
+   * If an extension has more than one changes to abandon that belong to the
+   * same project, they should use the batch instead of abandoning one by one.
+   * <p>
+   * It's the caller's responsibility to ensure that all jobs inside the same
+   * batch have the matching project from its ChangeControl. Violations will
+   * result in a ResourceConflictException.
+   */
+  public void batchAbandon(Project.NameKey project, CurrentUser user,
+      Collection<ChangeControl> controls, String msgTxt,
+      NotifyHandling notifyHandling) throws RestApiException, UpdateException {
+    if (controls.isEmpty()) {
+      return;
+    }
+    try (BatchUpdate u = batchUpdateFactory.create(
+        dbProvider.get(), project, user, TimeUtil.nowTs())) {
+      for (ChangeControl control : controls) {
+        if (!project.equals(control.getProject().getNameKey())) {
+          throw new ResourceConflictException(
+              String.format(
+                  "Project name \"%s\" doesn't match \"%s\"",
+                  control.getProject().getNameKey().get(),
+                  project.get()));
+        }
+        u.addOp(
+            control.getId(), new Op(control.getUser(), msgTxt, notifyHandling));
+      }
+      u.execute();
+    }
+  }
+
+  public void batchAbandon(Project.NameKey project, CurrentUser user,
+      Collection<ChangeControl> controls, String msgTxt)
+      throws RestApiException, UpdateException {
+    batchAbandon(project, user, controls, msgTxt, NotifyHandling.ALL);
+  }
+
+  public void batchAbandon(Project.NameKey project, CurrentUser user,
+      Collection<ChangeControl> controls)
+      throws RestApiException, UpdateException {
+    batchAbandon(project, user, controls, "", NotifyHandling.ALL);
+  }
+
   private class Op extends BatchUpdate.Op {
-    private final Account account;
     private final String msgTxt;
+    private final NotifyHandling notifyHandling;
+    private final Account account;
 
     private Change change;
     private PatchSet patchSet;
     private ChangeMessage message;
-    private NotifyHandling notifyHandling;
 
-    private Op(String msgTxt, Account account, NotifyHandling notifyHandling) {
-      this.account = account;
+    private Op(CurrentUser user, String msgTxt, NotifyHandling notifyHandling) {
       this.msgTxt = msgTxt;
       this.notifyHandling = notifyHandling;
+      account = user.isIdentifiedUser()
+          ? user.asIdentifiedUser().getAccount()
+          : null;
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
index 60d9c08..205d959 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.config.ChangeCleanupConfig;
 import com.google.gerrit.server.project.ChangeControl;
@@ -29,6 +31,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -37,10 +40,10 @@
   private static final Logger log = LoggerFactory.getLogger(AbandonUtil.class);
 
   private final ChangeCleanupConfig cfg;
-  private final InternalUser.Factory internalUserFactory;
   private final ChangeQueryProcessor queryProcessor;
   private final ChangeQueryBuilder queryBuilder;
   private final Abandon abandon;
+  private final InternalUser internalUser;
 
   @Inject
   AbandonUtil(
@@ -50,10 +53,10 @@
       ChangeQueryBuilder queryBuilder,
       Abandon abandon) {
     this.cfg = cfg;
-    this.internalUserFactory = internalUserFactory;
     this.queryProcessor = queryProcessor;
     this.queryBuilder = queryBuilder;
     this.abandon = abandon;
+    internalUser = internalUserFactory.create();
   }
 
   public void abandonInactiveOpenChanges() {
@@ -68,42 +71,42 @@
       if (!cfg.getAbandonIfMergeable()) {
         query += " -is:mergeable";
       }
-      List<ChangeData> changesToAbandon = queryProcessor.enforceVisibility(false)
-          .query(queryBuilder.parse(query)).entities();
-      int count = 0;
+
+      List<ChangeData> changesToAbandon =
+          queryProcessor
+              .enforceVisibility(false)
+              .query(queryBuilder.parse(query))
+              .entities();
+      ImmutableMultimap.Builder<Project.NameKey, ChangeControl> builder =
+          ImmutableMultimap.builder();
       for (ChangeData cd : changesToAbandon) {
+        ChangeControl control = cd.changeControl(internalUser);
+        builder.put(control.getProject().getNameKey(), control);
+      }
+
+      int count = 0;
+      Multimap<Project.NameKey, ChangeControl> abandons = builder.build();
+      String message = cfg.getAbandonMessage();
+      for (Project.NameKey project : abandons.keySet()) {
+        Collection<ChangeControl> changes = abandons.get(project);
         try {
-          if (noNeedToAbandon(cd, query)){
-            log.debug("Change data \"{}\" does not satisfy the query \"{}\" any"
-                + " more, hence skipping it in clean up", cd, query);
-            continue;
-          }
-          abandon.abandon(changeControl(cd), cfg.getAbandonMessage());
-          count++;
-        } catch (ResourceConflictException e) {
-          // Change was already merged or abandoned.
+          abandon.batchAbandon(project, internalUser, changes, message);
+          count += changes.size();
         } catch (Throwable e) {
-          log.error(String.format(
-              "Failed to auto-abandon inactive open change %d.",
-                  cd.getId().get()), e);
+          StringBuilder msg =
+              new StringBuilder("Failed to auto-abandon inactive change(s):");
+          for (ChangeControl change : changes) {
+            msg.append(" ").append(change.getId().get());
+          }
+          msg.append(".");
+          log.error(msg.toString(), e);
         }
       }
       log.info(String.format("Auto-Abandoned %d of %d changes.",
           count, changesToAbandon.size()));
     } catch (QueryParseException | OrmException e) {
-      log.error("Failed to query inactive open changes for auto-abandoning.", e);
+      log.error(
+          "Failed to query inactive open changes for auto-abandoning.", e);
     }
   }
-
-  private boolean noNeedToAbandon(ChangeData cd, String query)
-      throws OrmException, QueryParseException {
-    String newQuery = query + " change:" + cd.getId();
-    List<ChangeData> changesToAbandon = queryProcessor.enforceVisibility(false)
-        .query(queryBuilder.parse(newQuery)).entities();
-    return changesToAbandon.isEmpty();
-  }
-
-  private ChangeControl changeControl(ChangeData cd) throws OrmException {
-    return cd.changeControl(internalUserFactory.create());
-  }
 }
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 6333809..a3c992f 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
@@ -19,6 +19,8 @@
 import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -34,11 +36,11 @@
 import com.google.gerrit.server.ApprovalsUtil;
 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.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.CommentAdded;
 import com.google.gerrit.server.extensions.events.RevisionCreated;
-import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.BatchUpdate.Context;
@@ -48,6 +50,7 @@
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
@@ -62,7 +65,6 @@
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.ChangeIdUtil;
@@ -86,6 +88,7 @@
       LoggerFactory.getLogger(ChangeInserter.class);
 
   private final ProjectControl.GenericFactory projectControlFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final PatchSetUtil psUtil;
@@ -128,6 +131,7 @@
 
   @Inject
   ChangeInserter(ProjectControl.GenericFactory projectControlFactory,
+      IdentifiedUser.GenericFactory userFactory,
       ChangeControl.GenericFactory changeControlFactory,
       PatchSetInfoFactory patchSetInfoFactory,
       PatchSetUtil psUtil,
@@ -142,6 +146,7 @@
       @Assisted RevCommit commit,
       @Assisted String refName) {
     this.projectControlFactory = projectControlFactory;
+    this.userFactory = userFactory;
     this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.psUtil = psUtil;
@@ -353,8 +358,10 @@
     update.fixStatus(change.getStatus());
 
     LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes();
-    approvalsUtil.addReviewers(db, update, labelTypes, change,
-        patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet());
+    approvalsUtil.addReviewers(db, update, labelTypes, change, patchSet,
+        patchSetInfo,
+        filterOnChangeVisibility(db, ctx.getNotes(), reviewers),
+        Collections.<Account.Id> emptySet());
     approvalsUtil.addApprovals(db, update, labelTypes, patchSet,
         ctx.getControl(), approvals);
     if (message != null) {
@@ -368,6 +375,25 @@
     return true;
   }
 
+  private Set<Account.Id> filterOnChangeVisibility(final ReviewDb db,
+      final ChangeNotes notes, Set<Account.Id> accounts) {
+    return Sets.filter(accounts, new Predicate<Account.Id>() {
+      @Override
+      public boolean apply(Account.Id accountId) {
+        try {
+          IdentifiedUser user = userFactory.create(accountId);
+          return changeControlFactory.controlFor(notes, user).isVisible(db);
+        } catch (OrmException | NoSuchChangeException e) {
+          log.warn(
+              String.format("Failed to check if account %d can see change %d",
+                  accountId.get(), notes.getChangeId().get()),
+              e);
+          return false;
+        }
+      }
+    });
+  }
+
   @Override
   public void postUpdate(Context ctx) throws OrmException, NoSuchChangeException {
     if (sendMail) {
@@ -440,9 +466,6 @@
     try {
       RefControl refControl = projectControlFactory
           .controlFor(ctx.getProject(), ctx.getUser()).controlForRef(refName);
-      CommitValidators cv = commitValidatorsFactory.create(
-          refControl, new NoSshInfo(), ctx.getRepository());
-
       String refName = psId.toRefName();
       CommitReceivedEvent event = new CommitReceivedEvent(
           new ReceiveCommand(
@@ -453,19 +476,10 @@
           change.getDest().get(),
           commit,
           ctx.getIdentifiedUser());
-
-      switch (validatePolicy) {
-      case RECEIVE_COMMITS:
-        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(
-            ctx.getRepository(), ctx.getRevWalk());
-        cv.validateForReceiveCommits(event, rejectCommits);
-        break;
-      case GERRIT:
-        cv.validateForGerritCommits(event);
-        break;
-      case NONE:
-        break;
-      }
+      commitValidatorsFactory
+          .create(
+              validatePolicy, refControl, new NoSshInfo(), ctx.getRepository())
+          .validate(event);
     } catch (CommitValidationException e) {
       throw new ResourceConflictException(e.getFullMessage());
     } catch (NoSuchProjectException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 07714dc..e3a56b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -663,7 +663,7 @@
     //  - They are an explicit reviewer.
     //  - They ever voted on this change.
     Set<Account.Id> allUsers = new HashSet<>();
-    allUsers.addAll(cd.reviewers().all());
+    allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER));
     for (PatchSetApproval psa : cd.approvals().values()) {
       allUsers.add(psa.getAccountId());
     }
@@ -926,12 +926,19 @@
         .toSortedList(AccountInfoComparator.ORDER_NULLS_FIRST);
   }
 
+  @Nullable
+  private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException {
+    if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
+      return repoManager.openRepository(ctl.getProject().getNameKey());
+    }
+    return null;
+  }
+
   private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd,
       Map<PatchSet.Id, PatchSet> map) throws PatchListNotAvailableException,
       GpgException, OrmException, IOException {
     Map<String, RevisionInfo> res = new LinkedHashMap<>();
-    try (Repository repo =
-        repoManager.openRepository(ctl.getProject().getNameKey())) {
+    try (Repository repo = openRepoIfNecessary(ctl)) {
       for (PatchSet in : map.values()) {
         if ((has(ALL_REVISIONS)
             || in.getId().equals(ctl.getChange().currentPatchSetId()))
@@ -975,8 +982,7 @@
       throws PatchListNotAvailableException, GpgException, OrmException,
       IOException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
-    try (Repository repo =
-        repoManager.openRepository(ctl.getProject().getNameKey())) {
+    try (Repository repo = openRepoIfNecessary(ctl)) {
       RevisionInfo rev = toRevisionInfo(
           ctl, changeDataFactory.create(db.get(), ctl), in, repo, true);
       accountLoader.fill();
@@ -985,7 +991,7 @@
   }
 
   private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd,
-      PatchSet in, Repository repo, boolean fillCommit)
+      PatchSet in, @Nullable Repository repo, boolean fillCommit)
       throws PatchListNotAvailableException, GpgException, OrmException,
       IOException {
     Change c = ctl.getChange();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
index 2302b70..f0075ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -31,10 +32,11 @@
  * implementation changes, which might invalidate old entries).
  */
 public interface ChangeKindCache {
-  ChangeKind getChangeKind(ProjectState project, Repository repo,
+  ChangeKind getChangeKind(ProjectState project, @Nullable Repository repo,
       ObjectId prior, ObjectId next);
 
   ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch);
 
-  ChangeKind getChangeKind(Repository repo, ChangeData cd, PatchSet patch);
+  ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd,
+      PatchSet patch);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index edc1b12..b23bcf8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -22,9 +22,11 @@
 import com.google.common.cache.Cache;
 import com.google.common.cache.Weigher;
 import com.google.common.collect.FluentIterable;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -100,11 +102,13 @@
     }
 
     @Override
-    public ChangeKind getChangeKind(ProjectState project, Repository repo,
-        ObjectId prior, ObjectId next) {
+    public ChangeKind getChangeKind(ProjectState project,
+        @Nullable Repository repo, ObjectId prior, ObjectId next) {
       try {
         Key key = new Key(prior, next, useRecursiveMerge);
-        return new Loader(key, repo).call();
+        return new Loader(
+                key, repoManager, project.getProject().getNameKey(), repo)
+            .call();
       } catch (IOException e) {
         log.warn("Cannot check trivial rebase of new patch set " + next.name()
             + " in " + project.getProject().getName(), e);
@@ -120,7 +124,7 @@
     }
 
     @Override
-    public ChangeKind getChangeKind(Repository repo, ChangeData cd,
+    public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd,
         PatchSet patch) {
       return getChangeKindInternal(this, repo, cd, patch, projectCache);
     }
@@ -191,11 +195,16 @@
 
   private static class Loader implements Callable<ChangeKind> {
     private final Key key;
-    private final Repository repo;
+    private final GitRepositoryManager repoManager;
+    private final Project.NameKey projectName;
+    private final Repository alreadyOpenRepo;
 
-    private Loader(Key key, Repository repo) {
+    private Loader(Key key, GitRepositoryManager repoManager,
+        Project.NameKey projectName, @Nullable Repository alreadyOpenRepo) {
       this.key = key;
-      this.repo = repo;
+      this.repoManager = repoManager;
+      this.projectName = projectName;
+      this.alreadyOpenRepo = alreadyOpenRepo;
     }
 
     @Override
@@ -204,6 +213,12 @@
         return ChangeKind.NO_CODE_CHANGE;
       }
 
+      Repository repo = alreadyOpenRepo;
+      boolean close = false;
+      if (repo == null) {
+        repo = repoManager.openRepository(projectName);
+        close = true;
+      }
       try (RevWalk walk = new RevWalk(repo)) {
         RevCommit prior = walk.parseCommit(key.prior);
         walk.parseBody(prior);
@@ -246,6 +261,10 @@
           // it was a rework.
         }
         return ChangeKind.REWORK;
+      } finally {
+        if (close) {
+          repo.close();
+        }
       }
     }
 
@@ -321,11 +340,14 @@
   }
 
   @Override
-  public ChangeKind getChangeKind(ProjectState project, Repository repo,
-      ObjectId prior, ObjectId next) {
+  public ChangeKind getChangeKind(ProjectState project,
+      @Nullable Repository repo, ObjectId prior, ObjectId next) {
     try {
       Key key = new Key(prior, next, useRecursiveMerge);
-      return cache.get(key, new Loader(key, repo));
+      return cache.get(
+          key,
+          new Loader(
+                key, repoManager, project.getProject().getNameKey(), repo));
     } catch (ExecutionException e) {
       log.warn("Cannot check trivial rebase of new patch set " + next.name()
           + " in " + project.getProject().getName(), e);
@@ -340,14 +362,14 @@
   }
 
   @Override
-  public ChangeKind getChangeKind(Repository repo, ChangeData cd,
+  public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd,
       PatchSet patch) {
     return getChangeKindInternal(this, repo, cd, patch, projectCache);
   }
 
   private static ChangeKind getChangeKindInternal(
       ChangeKindCache cache,
-      Repository repo,
+      @Nullable Repository repo,
       ChangeData change,
       PatchSet patch,
       ProjectCache projectCache) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index db18ba2..0c620e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -236,7 +237,7 @@
     bu.addOp(destChange.getId(), inserter
         .setMessage("Uploaded patch set " + newPatchSetId.get() + ".")
         .setDraft(current.isDraft())
-        .setSendMail(false));
+        .setNotify(NotifyHandling.NONE));
     return destChange.getId();
   }
 
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 287c3ed..66ef731a 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
@@ -32,6 +32,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.FixInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.ProblemInfo.Status;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -530,7 +531,7 @@
         bu.addOp(ctl.getId(), inserter
             .setValidatePolicy(CommitValidators.Policy.NONE)
             .setFireRevisionCreated(false)
-            .setSendMail(false)
+            .setNotify(NotifyHandling.NONE)
             .setAllowClosed(true)
             .setMessage(
                 "Patch set for merged commit inserted by consistency checker"));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index bdefa93..18b7023 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -20,6 +20,8 @@
 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.api.changes.DeleteReviewerInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
@@ -38,7 +40,6 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.change.DeleteReviewer.Input;
 import com.google.gerrit.server.extensions.events.ReviewerDeleted;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -62,13 +63,11 @@
 import java.util.Map;
 
 @Singleton
-public class DeleteReviewer implements RestModifyView<ReviewerResource, Input> {
+public class DeleteReviewer
+    implements RestModifyView<ReviewerResource, DeleteReviewerInput> {
   private static final Logger log = LoggerFactory
       .getLogger(DeleteReviewer.class);
 
-  public static class Input {
-  }
-
   private final Provider<ReviewDb> dbProvider;
   private final ApprovalsUtil approvalsUtil;
   private final PatchSetUtil psUtil;
@@ -104,12 +103,19 @@
   }
 
   @Override
-  public Response<?> apply(ReviewerResource rsrc, Input input)
+  public Response<?> apply(ReviewerResource rsrc, DeleteReviewerInput input)
       throws RestApiException, UpdateException {
+    if (input == null) {
+      input = new DeleteReviewerInput();
+    }
+    if (input.notify == null) {
+      input.notify = NotifyHandling.ALL;
+    }
+
     try (BatchUpdate bu = batchUpdateFactory.create(dbProvider.get(),
         rsrc.getChangeResource().getProject(),
         rsrc.getChangeResource().getUser(), TimeUtil.nowTs())) {
-      Op op = new Op(rsrc.getReviewerUser().getAccount());
+      Op op = new Op(rsrc.getReviewerUser().getAccount(), input);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
     }
@@ -119,6 +125,7 @@
 
   private class Op extends BatchUpdate.Op {
     private final Account reviewer;
+    private final DeleteReviewerInput input;
     ChangeMessage changeMessage;
     Change currChange;
     PatchSet currPs;
@@ -126,8 +133,9 @@
     Map<String, Short> newApprovals = new HashMap<>();
     Map<String, Short> oldApprovals = new HashMap<>();
 
-    Op(Account reviewerAccount) {
+    Op(Account reviewerAccount, DeleteReviewerInput input) {
       this.reviewer = reviewerAccount;
+      this.input = input;
     }
 
     @Override
@@ -148,52 +156,56 @@
       }
 
       StringBuilder msg = new StringBuilder();
+      msg.append("Removed reviewer " + reviewer.getFullName());
+      StringBuilder removedVotesMsg = new StringBuilder();
+      removedVotesMsg.append(" with the following votes:\n\n");
+      boolean votesRemoved = false;
       for (PatchSetApproval a : approvals(ctx, reviewerId)) {
         if (ctx.getControl().canRemoveReviewer(a)) {
           del.add(a);
           if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) {
             oldApprovals.put(a.getLabel(), a.getValue());
-            if (msg.length() == 0) {
-              msg.append("Removed reviewer ").append(reviewer.getFullName())
-                  .append(" with the following votes:\n\n");
-            }
-            msg.append("* ").append(a.getLabel())
+            removedVotesMsg.append("* ").append(a.getLabel())
                 .append(formatLabelValue(a.getValue())).append(" by ")
                 .append(userFactory.create(a.getAccountId()).getNameEmail())
                 .append("\n");
+            votesRemoved = true;
           }
         } else {
           throw new AuthException("delete reviewer not permitted");
         }
       }
 
+      if (votesRemoved) {
+        msg.append(removedVotesMsg);
+      } else {
+        msg.append(".");
+      }
+
       ctx.getDb().patchSetApprovals().delete(del);
       ChangeUpdate update = ctx.getUpdate(currPs.getId());
       update.removeReviewer(reviewerId);
 
-      if (msg.length() > 0) {
-        changeMessage = new ChangeMessage(
-            new ChangeMessage.Key(currChange.getId(),
-                ChangeUtil.messageUUID(ctx.getDb())),
-            ctx.getAccountId(), ctx.getWhen(), currPs.getId());
-        changeMessage.setMessage(msg.toString());
-        cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
-      }
+      changeMessage = new ChangeMessage(
+          new ChangeMessage.Key(currChange.getId(),
+              ChangeUtil.messageUUID(ctx.getDb())),
+          ctx.getAccountId(), ctx.getWhen(), currPs.getId());
+      changeMessage.setMessage(msg.toString());
+      cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
 
       return true;
     }
 
     @Override
     public void postUpdate(Context ctx) {
-      if (changeMessage == null) {
-        return;
+      if (input.notify.compareTo(NotifyHandling.NONE) > 0) {
+        emailReviewers(ctx.getProject(), currChange, del, changeMessage);
       }
-
-      emailReviewers(ctx.getProject(), currChange, del, changeMessage);
       reviewerDeleted.fire(currChange, currPs, reviewer,
           ctx.getAccount(),
           changeMessage.getMessage(),
           newApprovals, oldApprovals,
+          input.notify,
           ctx.getWhen());
     }
 
@@ -233,30 +245,31 @@
       }
       return Short.toString(value);
     }
-  }
 
-  private void emailReviewers(Project.NameKey projectName, Change change,
-      List<PatchSetApproval> dels, ChangeMessage changeMessage) {
+    private void emailReviewers(Project.NameKey projectName, Change change,
+        List<PatchSetApproval> dels, ChangeMessage changeMessage) {
 
-    // The user knows they removed themselves, don't bother emailing them.
-    List<Account.Id> toMail = Lists.newArrayListWithCapacity(dels.size());
-    Account.Id userId = user.get().getAccountId();
-    for (PatchSetApproval psa : dels) {
-      if (!psa.getAccountId().equals(userId)) {
-        toMail.add(psa.getAccountId());
+      // The user knows they removed themselves, don't bother emailing them.
+      List<Account.Id> toMail = Lists.newArrayListWithCapacity(dels.size());
+      Account.Id userId = user.get().getAccountId();
+      for (PatchSetApproval psa : dels) {
+        if (!psa.getAccountId().equals(userId)) {
+          toMail.add(psa.getAccountId());
+        }
       }
-    }
-    if (!toMail.isEmpty()) {
-      try {
-        DeleteReviewerSender cm =
-            deleteReviewerSenderFactory.create(projectName, change.getId());
-        cm.setFrom(userId);
-        cm.addReviewers(toMail);
-        cm.setChangeMessage(changeMessage.getMessage(),
-            changeMessage.getWrittenOn());
-        cm.send();
-      } catch (Exception err) {
-        log.error("Cannot email update for change " + change.getId(), err);
+      if (!toMail.isEmpty()) {
+        try {
+          DeleteReviewerSender cm =
+              deleteReviewerSenderFactory.create(projectName, change.getId());
+          cm.setFrom(userId);
+          cm.addReviewers(toMail);
+          cm.setChangeMessage(changeMessage.getMessage(),
+              changeMessage.getWrittenOn());
+          cm.setNotify(input.notify);
+          cm.send();
+        } catch (Exception err) {
+          log.error("Cannot email update for change " + change.getId(), err);
+        }
       }
     }
   }
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 f1bdba5..e25e2292 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,8 +14,9 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 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.api.changes.DeleteVoteInput;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -28,6 +29,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -44,6 +46,7 @@
 import com.google.gerrit.server.mail.DeleteVoteSender;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -137,64 +140,66 @@
       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
+      boolean found = false;
       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())
-                    .getNameEmail())
-                .append("\n");
-            psa = a;
-            a.setValue((short)0);
-            ctx.getUpdate(psId).removeApprovalFor(a.getAccountId(), label);
-            break;
-          }
-        } else {
+          ctx.getDb(), ctl, psId, accountId)) {
+        if (labelTypes.byLabel(a.getLabelId()) == null) {
+          continue; // Ignore undefined labels.
+        } else if (!a.getLabel().equals(label)) {
+          // Populate map for non-matching labels, needed by VoteDeleted.
+          newApprovals.put(a.getLabel(), a.getValue());
+          continue;
+        } else if (!ctl.canRemoveReviewer(a)) {
           throw new AuthException("delete vote not permitted");
         }
+        // Set the approval to 0 if vote is being removed.
+        newApprovals.put(a.getLabel(), (short) 0);
+        found = true;
+
+        // Set old value, as required by VoteDeleted.
+        oldApprovals.put(a.getLabel(), a.getValue());
+        break;
       }
-      if (psa == null) {
+      if (!found) {
         throw new ResourceNotFoundException();
       }
-      ctx.getDb().patchSetApprovals().update(Collections.singleton(psa));
 
-      if (msg.length() > 0) {
-        changeMessage =
-            new ChangeMessage(new ChangeMessage.Key(change.getId(),
-                ChangeUtil.messageUUID(ctx.getDb())),
-                ctx.getAccountId(),
-                ctx.getWhen(),
-                change.currentPatchSetId());
-        changeMessage.setMessage(msg.toString());
-        cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId),
-            changeMessage);
-      }
+      ctx.getUpdate(psId).removeApprovalFor(accountId, label);
+      ctx.getDb().patchSetApprovals().upsert(
+          Collections.singleton(deletedApproval(ctx)));
+
+      changeMessage =
+          new ChangeMessage(new ChangeMessage.Key(change.getId(),
+              ChangeUtil.messageUUID(ctx.getDb())),
+              ctx.getAccountId(),
+              ctx.getWhen(),
+              change.currentPatchSetId());
+      StringBuilder msg = new StringBuilder();
+      msg.append("Removed ");
+      LabelVote.appendTo(msg, label, checkNotNull(oldApprovals.get(label)));
+      changeMessage.setMessage(
+          msg.append(" by ")
+              .append(userFactory.create(accountId).getNameEmail())
+              .append("\n")
+              .toString());
+      cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId),
+          changeMessage);
+
       return true;
     }
 
+    private PatchSetApproval deletedApproval(ChangeContext ctx) {
+      return new PatchSetApproval(
+          new PatchSetApproval.Key(
+              ps.getId(),
+              accountId,
+              new LabelId(label)),
+          (short) 0,
+          ctx.getWhen());
+    }
+
     @Override
     public void postUpdate(Context ctx) {
       if (changeMessage == null) {
@@ -220,11 +225,4 @@
           user.getAccount(), ctx.getWhen());
     }
   }
-
-  private static String formatLabelValue(short value) {
-    if (value > 0) {
-      return "+" + value;
-    }
-    return Short.toString(value);
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index e0591f4..8e55df5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.gerrit.server.util.GitUtil.getParent;
-
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.common.FileInfo;
@@ -23,7 +21,6 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -32,24 +29,18 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
 
-import java.io.IOException;
 import java.util.Map;
 import java.util.TreeMap;
 
 @Singleton
 public class FileInfoJson {
   private final PatchListCache patchListCache;
-  private final GitRepositoryManager repoManager;
 
   @Inject
   FileInfoJson(
-      PatchListCache patchListCache,
-      GitRepositoryManager repoManager) {
-    this.repoManager = repoManager;
+      PatchListCache patchListCache) {
     this.patchListCache = patchListCache;
   }
 
@@ -64,24 +55,19 @@
         ? null
         : ObjectId.fromString(base.getRevision().get());
     ObjectId b = ObjectId.fromString(revision.get());
-    return toFileInfoMap(change, a, b);
+    return toFileInfoMap(change, new PatchListKey(a, b, Whitespace.IGNORE_NONE));
   }
 
   Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, int parent)
-      throws RepositoryNotFoundException, IOException,
-          PatchListNotAvailableException {
+      throws PatchListNotAvailableException {
     ObjectId b = ObjectId.fromString(revision.get());
-    ObjectId a;
-    try (Repository git = repoManager.openRepository(change.getProject())) {
-      a = getParent(git, b, parent);
-    }
-    return toFileInfoMap(change, a, b);
+    return toFileInfoMap(change,
+        PatchListKey.againstParentNum(parent + 1, b, Whitespace.IGNORE_NONE));
   }
 
   private Map<String, FileInfo> toFileInfoMap(Change change,
-      ObjectId a, ObjectId b) throws PatchListNotAvailableException {
-    PatchList list = patchListCache.get(
-        new PatchListKey(a, b, Whitespace.IGNORE_NONE), change.getProject());
+      PatchListKey key) throws PatchListNotAvailableException {
+    PatchList list = patchListCache.get(key, change.getProject());
 
     Map<String, FileInfo> files = new TreeMap<>();
     for (PatchListEntry e : list.getPatches()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index 35dbec1..c077bbb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -138,9 +138,10 @@
     }
 
     @Override
-    public Response<?> apply(RevisionResource resource) throws AuthException,
-        BadRequestException, ResourceNotFoundException, OrmException,
-        RepositoryNotFoundException, IOException {
+    public Response<?> apply(RevisionResource resource)
+        throws AuthException, BadRequestException, ResourceNotFoundException,
+        OrmException, RepositoryNotFoundException, IOException,
+        PatchListNotAvailableException {
       checkOptions();
       if (reviewed) {
         return Response.ok(reviewed(resource));
@@ -149,26 +150,22 @@
       }
 
       Response<Map<String, FileInfo>> r;
-      try {
-        if (base != null) {
-          RevisionResource baseResource = revisions.parse(
-              resource.getChangeResource(), IdString.fromDecoded(base));
-          r = Response.ok(fileInfoJson.toFileInfoMap(
-              resource.getChange(),
-              resource.getPatchSet().getRevision(),
-              baseResource.getPatchSet()));
-        } else if (parentNum > 0) {
-          r = Response.ok(fileInfoJson.toFileInfoMap(
-              resource.getChange(),
-              resource.getPatchSet().getRevision(),
-              parentNum - 1));
-        } else {
-          r = Response.ok(fileInfoJson.toFileInfoMap(
-              resource.getChange(),
-              resource.getPatchSet()));
-        }
-      } catch (PatchListNotAvailableException e) {
-        throw new ResourceNotFoundException(e.getMessage());
+      if (base != null) {
+        RevisionResource baseResource = revisions.parse(
+            resource.getChangeResource(), IdString.fromDecoded(base));
+        r = Response.ok(fileInfoJson.toFileInfoMap(
+            resource.getChange(),
+            resource.getPatchSet().getRevision(),
+            baseResource.getPatchSet()));
+      } else if (parentNum > 0) {
+        r = Response.ok(fileInfoJson.toFileInfoMap(
+            resource.getChange(),
+            resource.getPatchSet().getRevision(),
+            parentNum - 1));
+      } else {
+        r = Response.ok(fileInfoJson.toFileInfoMap(
+            resource.getChange(),
+            resource.getPatchSet()));
       }
 
       if (resource.isCacheable()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
index 8c9a0ad..e51d37b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -35,7 +35,7 @@
   private final GitRepositoryManager repoManager;
   private final ChangeJson.Factory json;
 
-  @Option(name = "--links", usage = "Add weblinks")
+  @Option(name = "--links", usage = "Include weblinks")
   private boolean addLinks;
 
   @Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java
new file mode 100644
index 0000000..4e94935
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java
@@ -0,0 +1,105 @@
+// 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.common.collect.ImmutableList;
+import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.CacheControl;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+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.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class GetMergeList implements RestReadView<RevisionResource> {
+  private final GitRepositoryManager repoManager;
+  private final ChangeJson.Factory json;
+
+  @Option(name = "--parent", usage = "Uninteresting parent (1-based, default = 1)")
+  private int uninterestingParent = 1;
+
+  @Option(name = "--links", usage = "Include weblinks")
+  private boolean addLinks;
+
+  @Inject
+  GetMergeList(GitRepositoryManager repoManager, ChangeJson.Factory json) {
+    this.repoManager = repoManager;
+    this.json = json;
+  }
+
+  public void setUninterestingParent(int uninterestingParent) {
+    this.uninterestingParent = uninterestingParent;
+  }
+
+  public void setAddLinks(boolean addLinks) {
+    this.addLinks = addLinks;
+  }
+
+  @Override
+  public Response<List<CommitInfo>> apply(RevisionResource rsrc)
+      throws BadRequestException, IOException {
+    List<CommitInfo> result = new ArrayList<>();
+    Project.NameKey p = rsrc.getChange().getProject();
+    try (Repository repo = repoManager.openRepository(p);
+        RevWalk rw = new RevWalk(repo)) {
+      String rev = rsrc.getPatchSet().getRevision().get();
+      RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
+      rw.parseBody(commit);
+
+      if (uninterestingParent < 1
+          || uninterestingParent > commit.getParentCount()) {
+        throw new BadRequestException("No such parent: " + uninterestingParent);
+      }
+
+      if (commit.getParentCount() < 2) {
+        return Response.<List<CommitInfo>> ok(ImmutableList.<CommitInfo> of());
+      }
+
+      for (int parent = 0; parent < commit.getParentCount(); parent++) {
+        if (parent == uninterestingParent - 1) {
+          rw.markUninteresting(commit.getParent(parent));
+        } else {
+          rw.markStart(commit.getParent(parent));
+        }
+      }
+
+      ChangeJson changeJson = json.create(ChangeJson.NO_OPTIONS);
+      RevCommit c;
+      while ((c = rw.next()) != null) {
+        CommitInfo info =
+            changeJson.toCommit(rsrc.getControl(), rw, c, addLinks, true);
+        result.add(info);
+      }
+    }
+
+    Response<List<CommitInfo>> r = Response.ok(result);
+    if (rsrc.isCacheable()) {
+      r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
+    }
+    return r;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 12e4276..0a7452b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -92,6 +92,9 @@
     PatchSet basePs = isEdit
         ? rsrc.getEdit().get().getBasePatchSet()
         : rsrc.getPatchSet();
+
+    reloadChangeIfStale(cds, basePs);
+
     for (PatchSetData d : sorter.sort(cds, basePs)) {
       PatchSet ps = d.patchSet();
       RevCommit commit;
@@ -123,6 +126,17 @@
     return result;
   }
 
+  private void reloadChangeIfStale(List<ChangeData> cds, PatchSet wantedPs)
+      throws OrmException {
+    for (ChangeData cd : cds) {
+      if (cd.getId().equals(wantedPs.getId().getParentKey())) {
+        if (cd.patchSet(wantedPs.getId()) == null) {
+          cd.reloadChange();
+        }
+      }
+    }
+  }
+
   public static class RelatedInfo {
     public List<ChangeAndCommit> changes;
   }
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 6de7deb..7f630ba 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
@@ -78,6 +78,7 @@
     child(CHANGE_KIND, "reviewers").to(Reviewers.class);
     get(REVIEWER_KIND).to(GetReviewer.class);
     delete(REVIEWER_KIND).to(DeleteReviewer.class);
+    post(REVIEWER_KIND, "delete").to(DeleteReviewer.class);
     child(REVIEWER_KIND, "votes").to(Votes.class);
     delete(VOTE_KIND).to(DeleteVote.class);
     post(VOTE_KIND, "delete").to(DeleteVote.class);
@@ -99,6 +100,7 @@
     post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
     post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
     get(REVISION_KIND, "archive").to(GetArchive.class);
+    get(REVISION_KIND, "mergelist").to(GetMergeList.class);
 
     child(REVISION_KIND, "drafts").to(DraftComments.class);
     put(REVISION_KIND, "drafts").to(CreateDraftComment.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 508d380..3f38fc3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -35,7 +35,6 @@
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.RevisionCreated;
-import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.BatchUpdate.Context;
@@ -47,13 +46,11 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.ssh.NoSshInfo;
-import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
@@ -91,14 +88,13 @@
   private final ChangeControl origCtl;
 
   // Fields exposed as setters.
-  private SshInfo sshInfo;
   private String message;
   private CommitValidators.Policy validatePolicy =
       CommitValidators.Policy.GERRIT;
   private boolean draft;
   private List<String> groups = Collections.emptyList();
   private boolean fireRevisionCreated = true;
-  private boolean sendMail = true;
+  private NotifyHandling notify = NotifyHandling.ALL;
   private boolean allowClosed;
   private boolean copyApprovals = true;
 
@@ -144,11 +140,6 @@
     return this;
   }
 
-  public PatchSetInserter setSshInfo(SshInfo sshInfo) {
-    this.sshInfo = sshInfo;
-    return this;
-  }
-
   public PatchSetInserter setValidatePolicy(CommitValidators.Policy validate) {
     this.validatePolicy = checkNotNull(validate);
     return this;
@@ -170,8 +161,8 @@
     return this;
   }
 
-  public PatchSetInserter setSendMail(boolean sendMail) {
-    this.sendMail = sendMail;
+  public PatchSetInserter setNotify(NotifyHandling notify) {
+    this.notify = notify;
     return this;
   }
 
@@ -198,7 +189,6 @@
   @Override
   public void updateRepo(RepoContext ctx)
       throws AuthException, ResourceConflictException, IOException, OrmException {
-    init();
     validate(ctx);
     ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(),
         commit, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE));
@@ -230,7 +220,7 @@
     patchSet = psUtil.insert(db, ctx.getRevWalk(), ctx.getUpdate(psId),
         psId, commit, draft, newGroups, null);
 
-    if (sendMail) {
+    if (notify != NotifyHandling.NONE) {
       oldReviewers = approvalsUtil.getReviewers(db, ctl.getNotes());
     }
 
@@ -257,7 +247,7 @@
 
   @Override
   public void postUpdate(Context ctx) throws OrmException {
-    if (sendMail) {
+    if (notify != NotifyHandling.NONE) {
       try {
         ReplacePatchSetSender cm = replacePatchSetFactory.create(
             ctx.getProject(), change.getId());
@@ -266,6 +256,7 @@
         cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
         cm.addReviewers(oldReviewers.byState(REVIEWER));
         cm.addExtraCC(oldReviewers.byState(CC));
+        cm.setNotify(notify);
         cm.send();
       } catch (Exception err) {
         log.error("Cannot send email for new patch set on change "
@@ -273,30 +264,21 @@
       }
     }
 
-    NotifyHandling notify = sendMail
-        ? NotifyHandling.ALL
-        : NotifyHandling.NONE;
     if (fireRevisionCreated) {
       revisionCreated.fire(change, patchSet, ctx.getAccountId(),
           ctx.getWhen(), notify);
     }
   }
 
-  private void init() {
-    if (sshInfo == null) {
-      sshInfo = new NoSshInfo();
-    }
-  }
-
   private void validate(RepoContext ctx)
       throws AuthException, ResourceConflictException, IOException,
       OrmException {
-    CommitValidators cv = commitValidatorsFactory.create(
-        origCtl.getRefControl(), sshInfo, ctx.getRepository());
-
     if (!origCtl.canAddPatchSet(ctx.getDb())) {
       throw new AuthException("cannot add patch set");
     }
+    if (validatePolicy == CommitValidators.Policy.NONE) {
+      return;
+    }
 
     String refName = getPatchSetId().toRefName();
     CommitReceivedEvent event = new CommitReceivedEvent(
@@ -309,18 +291,11 @@
         commit, ctx.getIdentifiedUser());
 
     try {
-      switch (validatePolicy) {
-      case RECEIVE_COMMITS:
-        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(
-            ctx.getRepository(), ctx.getRevWalk());
-        cv.validateForReceiveCommits(event, rejectCommits);
-        break;
-      case GERRIT:
-        cv.validateForGerritCommits(event);
-        break;
-      case NONE:
-        break;
-      }
+      commitValidatorsFactory
+          .create(
+              validatePolicy, origCtl.getRefControl(), new NoSshInfo(),
+              ctx.getRepository())
+          .validate(event);
     } catch (CommitValidationException e) {
       throw new ResourceConflictException(e.getFullMessage());
     }
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 aa35da8..3828041 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
@@ -67,6 +67,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.extensions.events.CommentAdded;
 import com.google.gerrit.server.git.BatchUpdate;
@@ -212,7 +213,7 @@
       }
       bu.addOp(
           revision.getChange().getId(),
-          new Op(revision.getPatchSet().getId(), input));
+          new Op(revision.getPatchSet().getId(), input, reviewerResults));
       bu.execute();
 
       for (PostReviewers.Addition reviewerResult : reviewerResults) {
@@ -387,6 +388,7 @@
   private class Op extends BatchUpdate.Op {
     private final PatchSet.Id psId;
     private final ReviewInput in;
+    private final List<PostReviewers.Addition> reviewerResults;
 
     private IdentifiedUser user;
     private ChangeNotes notes;
@@ -397,9 +399,11 @@
     private Map<String, Short> approvals = new HashMap<>();
     private Map<String, Short> oldApprovals = new HashMap<>();
 
-    private Op(PatchSet.Id psId, ReviewInput in) {
+    private Op(PatchSet.Id psId, ReviewInput in,
+        List<PostReviewers.Addition> reviewerResults) {
       this.psId = psId;
       this.in = in;
+      this.reviewerResults = reviewerResults;
     }
 
     @Override
@@ -615,6 +619,28 @@
       return previous;
     }
 
+    private boolean isReviewer(ChangeContext ctx) throws OrmException {
+      if (ctx.getAccountId().equals(ctx.getChange().getOwner())) {
+        return true;
+      }
+      for (PostReviewers.Addition addition : reviewerResults) {
+        if (addition.op.addedReviewers == null) {
+          continue;
+        }
+        for (PatchSetApproval psa : addition.op.addedReviewers) {
+          if (psa.getAccountId().equals(ctx.getAccountId())) {
+            return true;
+          }
+        }
+      }
+      ChangeData cd = changeDataFactory.create(db.get(), ctx.getControl());
+      ReviewerSet reviewers = cd.reviewers();
+      if (reviewers.byState(REVIEWER).contains(ctx.getAccountId())) {
+        return true;
+      }
+      return false;
+    }
+
     private boolean updateLabels(ChangeContext ctx)
         throws OrmException, ResourceConflictException {
       Map<String, Short> inLabels = MoreObjects.firstNonNull(in.labels,
@@ -689,6 +715,14 @@
           && ctx.getChange().getStatus().isClosed()) {
         throw new ResourceConflictException("change is closed");
       }
+
+      // Return early if user is not a reviewer and not posting any labels.
+      // This allows us to preserve their CC status.
+      if (current.isEmpty() && del.isEmpty() && ups.isEmpty() &&
+          !isReviewer(ctx)) {
+        return false;
+      }
+
       forceCallerAsReviewer(ctx, current, ups, del);
       ctx.getDb().patchSetApprovals().delete(del);
       ctx.getDb().patchSetApprovals().upsert(ups);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index fb37d9d..be94d5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.extensions.client.ReviewerState.CC;
 import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
 
+import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
@@ -40,7 +41,6 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gerrit.server.account.AccountsCollection;
 import com.google.gerrit.server.account.GroupMembers;
@@ -96,7 +96,6 @@
   private final Provider<IdentifiedUser> user;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final Config cfg;
-  private final AccountCache accountCache;
   private final ReviewerJson json;
   private final ReviewerAdded reviewerAdded;
   private final NotesMigration migration;
@@ -115,7 +114,6 @@
       Provider<IdentifiedUser> user,
       IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritServerConfig Config cfg,
-      AccountCache accountCache,
       ReviewerJson json,
       ReviewerAdded reviewerAdded,
       NotesMigration migration) {
@@ -132,7 +130,6 @@
     this.user = user;
     this.identifiedUserFactory = identifiedUserFactory;
     this.cfg = cfg;
-    this.accountCache = accountCache;
     this.json = json;
     this.reviewerAdded = reviewerAdded;
     this.migration = migration;
@@ -184,7 +181,12 @@
       return new Addition(reviewer, rsrc.getChangeResource(),
           ImmutableMap.of(member.getId(), control), state);
     }
-    throw new UnprocessableEntityException("Change not visible to " + reviewer);
+    if (member.isActive()) {
+      throw new UnprocessableEntityException(
+          String.format("Change not visible to %s", reviewer));
+    }
+    throw new UnprocessableEntityException(
+        String.format("Account of %s is inactive.", reviewer));
   }
 
   private Addition putGroup(ChangeResource rsrc, AddReviewerInput input)
@@ -355,11 +357,15 @@
         }
         emailReviewers(rsrc.getChange(), addedReviewers, addedCCs);
         if (!addedReviewers.isEmpty()) {
-          for (PatchSetApproval psa : addedReviewers) {
-            Account account = accountCache.get(psa.getAccountId()).getAccount();
-            reviewerAdded.fire(rsrc.getChange(), patchSet, account,
+          List<Account.Id> reviewers = Lists.transform(addedReviewers,
+              new Function<PatchSetApproval, Account.Id>() {
+                @Override
+                public Account.Id apply(PatchSetApproval psa) {
+                  return psa.getAccountId();
+                }
+              });
+          reviewerAdded.fire(rsrc.getChange(), patchSet, reviewers,
               ctx.getAccount(), ctx.getWhen());
-          }
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
index c86e98f..76ff15e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Optional;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.AcceptsPost;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -71,9 +72,8 @@
   }
 
   @Singleton
-  public static class Publish implements RestModifyView<ChangeResource, Publish.Input> {
-    public static class Input {
-    }
+  public static class Publish
+      implements RestModifyView<ChangeResource, PublishChangeEditInput> {
 
     private final ChangeEditUtil editUtil;
 
@@ -83,7 +83,7 @@
     }
 
     @Override
-    public Response<?> apply(ChangeResource rsrc, Publish.Input in)
+    public Response<?> apply(ChangeResource rsrc, PublishChangeEditInput in)
         throws NoSuchChangeException, IOException, OrmException,
         RestApiException, UpdateException {
       Capable r =
@@ -98,7 +98,10 @@
             "no edit exists for change %s",
             rsrc.getChange().getChangeId()));
       }
-      editUtil.publish(edit.get());
+      if (in == null) {
+        in = new PublishChangeEditInput();
+      }
+      editUtil.publish(edit.get(), in.notify);
       return Response.none();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 8909e60..d3c300e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -150,7 +151,7 @@
     patchSetInserter = patchSetInserterFactory
         .create(ctl, rebasedPatchSetId, rebasedCommit)
         .setDraft(originalPatchSet.isDraft())
-        .setSendMail(false)
+        .setNotify(NotifyHandling.NONE)
         .setFireRevisionCreated(fireRevisionCreated)
         .setCopyApprovals(copyApprovals)
         .setMessage(
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 4750197..ba12fed 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
@@ -134,7 +134,7 @@
   private final ChangeMessagesUtil cmUtil;
   private final ChangeNotes.Factory changeNotesFactory;
   private final Provider<MergeOp> mergeOpProvider;
-  private final MergeSuperSet mergeSuperSet;
+  private final Provider<MergeSuperSet> mergeSuperSet;
   private final AccountsCollection accounts;
   private final ChangesCollection changes;
   private final String label;
@@ -154,7 +154,7 @@
       ChangeMessagesUtil cmUtil,
       ChangeNotes.Factory changeNotesFactory,
       Provider<MergeOp> mergeOpProvider,
-      MergeSuperSet mergeSuperSet,
+      Provider<MergeSuperSet> mergeSuperSet,
       AccountsCollection accounts,
       ChangesCollection changes,
       @GerritServerConfig Config cfg,
@@ -345,7 +345,7 @@
 
     ChangeSet cs;
     try {
-      cs = mergeSuperSet.completeChangeSet(
+      cs = mergeSuperSet.get().completeChangeSet(
           db, cd.change(), resource.getControl().getUser());
     } catch (OrmException | IOException e) {
       throw new OrmRuntimeException("Could not determine complete set of " +
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
index c4c0e98..1afe960 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
@@ -54,7 +54,7 @@
   private final ChangeJson.Factory json;
   private final Provider<ReviewDb> dbProvider;
   private final Provider<InternalChangeQuery> queryProvider;
-  private final MergeSuperSet mergeSuperSet;
+  private final Provider<MergeSuperSet> mergeSuperSet;
   private final Provider<WalkSorter> sorter;
 
   @Option(name = "-o", usage = "Output options")
@@ -66,7 +66,7 @@
   SubmittedTogether(ChangeJson.Factory json,
       Provider<ReviewDb> dbProvider,
       Provider<InternalChangeQuery> queryProvider,
-      MergeSuperSet mergeSuperSet,
+      Provider<MergeSuperSet> mergeSuperSet,
       Provider<WalkSorter> sorter) {
     this.json = json;
     this.dbProvider = dbProvider;
@@ -96,7 +96,7 @@
 
       if (c.getStatus().isOpen()) {
         ChangeSet cs =
-            mergeSuperSet.completeChangeSet(
+            mergeSuperSet.get().completeChangeSet(
                 dbProvider.get(), c, resource.getControl().getUser());
         cds = cs.changes().asList();
         hidden = cs.nonVisibleChanges().size();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AgreementJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AgreementJson.java
new file mode 100644
index 0000000..3ababbc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AgreementJson.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.common.AgreementInfo;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupJson;
+import com.google.gerrit.server.group.GroupResource;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AgreementJson {
+  private static final Logger log =
+      LoggerFactory.getLogger(AgreementJson.class);
+
+  private final Provider<CurrentUser> self;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final GroupControl.GenericFactory genericGroupControlFactory;
+  private final GroupJson groupJson;
+
+  @Inject
+  AgreementJson(Provider<CurrentUser> self,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      GroupControl.GenericFactory genericGroupControlFactory,
+      GroupJson groupJson) {
+    this.self = self;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.genericGroupControlFactory = genericGroupControlFactory;
+    this.groupJson = groupJson;
+  }
+
+  public AgreementInfo format(ContributorAgreement ca) {
+    AgreementInfo info = new AgreementInfo();
+    info.name = ca.getName();
+    info.description = ca.getDescription();
+    info.url = ca.getAgreementUrl();
+    GroupReference autoVerifyGroup = ca.getAutoVerify();
+    if (autoVerifyGroup != null && self.get().isIdentifiedUser()) {
+      IdentifiedUser user =
+          identifiedUserFactory.create(self.get().getAccountId());
+      try {
+        GroupControl gc = genericGroupControlFactory.controlFor(
+            user, autoVerifyGroup.getUUID());
+        GroupResource group = new GroupResource(gc);
+        info.autoVerifyGroup = groupJson.format(group);
+      } catch (NoSuchGroupException | OrmException e) {
+        log.warn("autoverify group \"" + autoVerifyGroup.getName() +
+            "\" does not exist, referenced in CLA \"" + ca.getName() + "\"");
+      }
+    }
+    return info;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index f2fc94e..5a40a31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.gwtjsonrpc.server.XsrfException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
index 8e181a9..5b0f73d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.config;
 
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.DefaultRealm;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.auth.AuthBackend;
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 da1f9a6..c91ca3b 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
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 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.
@@ -116,7 +116,6 @@
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
-import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.git.validators.MergeValidationListener;
 import com.google.gerrit.server.git.validators.MergeValidators;
 import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
@@ -134,6 +133,8 @@
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.FromAddressGenerator;
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
+import com.google.gerrit.server.mail.MailSoyTofuProvider;
+import com.google.gerrit.server.mail.MailTemplates;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
@@ -170,6 +171,7 @@
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
 import com.google.inject.internal.UniqueAnnotations;
+import com.google.template.soy.tofu.SoyTofu;
 
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.lib.Config;
@@ -275,6 +277,9 @@
 
     bind(RuntimeInstance.class)
         .toProvider(VelocityRuntimeProvider.class);
+    bind(SoyTofu.class)
+        .annotatedWith(MailTemplates.class)
+        .toProvider(MailSoyTofuProvider.class);
     bind(FromAddressGenerator.class).toProvider(
         FromAddressGeneratorProvider.class).in(SINGLETON);
     bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class)
@@ -366,7 +371,6 @@
 
     bind(AnonymousUser.class);
 
-    factory(CommitValidators.Factory.class);
     factory(RefOperationValidators.Factory.class);
     factory(MergeValidators.Factory.class);
     factory(ProjectConfigValidator.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
index 1dc910c..9e2ad77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -20,6 +20,18 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.extensions.common.AuthInfo;
+import com.google.gerrit.extensions.common.ChangeConfigInfo;
+import com.google.gerrit.extensions.common.DownloadInfo;
+import com.google.gerrit.extensions.common.DownloadSchemeInfo;
+import com.google.gerrit.extensions.common.GerritInfo;
+import com.google.gerrit.extensions.common.PluginConfigInfo;
+import com.google.gerrit.extensions.common.ReceiveInfo;
+import com.google.gerrit.extensions.common.ServerInfo;
+import com.google.gerrit.extensions.common.SshdInfo;
+import com.google.gerrit.extensions.common.SuggestInfo;
+import com.google.gerrit.extensions.common.UserConfigInfo;
 import com.google.gerrit.extensions.config.CloneCommand;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
@@ -28,8 +40,6 @@
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.EnableSignedPush;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.avatar.AvatarProvider;
@@ -38,14 +48,15 @@
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.documentation.QueryDocumentationExecutor;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
 
 import java.net.MalformedURLException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -69,6 +80,8 @@
   private final boolean enableSignedPush;
   private final QueryDocumentationExecutor docSearcher;
   private final NotesMigration migration;
+  private final ProjectCache projectCache;
+  private final AgreementJson agreementJson;
 
   @Inject
   public GetServerInfo(
@@ -86,7 +99,9 @@
       DynamicItem<AvatarProvider> avatar,
       @EnableSignedPush boolean enableSignedPush,
       QueryDocumentationExecutor docSearcher,
-      NotesMigration migration) {
+      NotesMigration migration,
+      ProjectCache projectCache,
+      AgreementJson agreementJson) {
     this.config = config;
     this.authConfig = authConfig;
     this.realm = realm;
@@ -102,6 +117,8 @@
     this.enableSignedPush = enableSignedPush;
     this.docSearcher = docSearcher;
     this.migration = migration;
+    this.projectCache = projectCache;
+    this.agreementJson = agreementJson;
   }
 
   @Override
@@ -134,6 +151,18 @@
     info.switchAccountUrl = cfg.getSwitchAccountUrl();
     info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth());
 
+    if (info.useContributorAgreements != null) {
+      Collection<ContributorAgreement> agreements =
+          projectCache.getAllProjects().getConfig().getContributorAgreements();
+      if (!agreements.isEmpty()) {
+        info.contributorAgreements =
+            Lists.newArrayListWithCapacity(agreements.size());
+        for (ContributorAgreement agreement: agreements) {
+          info.contributorAgreements.add(agreementJson.format(agreement));
+        }
+      }
+    }
+
     switch (info.authType) {
       case LDAP:
       case LDAP_BIND:
@@ -322,85 +351,4 @@
   private static Boolean toBoolean(boolean v) {
     return v ? v : null;
   }
-
-  public static class ServerInfo {
-    public AuthInfo auth;
-    public ChangeConfigInfo change;
-    public DownloadInfo download;
-    public GerritInfo gerrit;
-    public Boolean noteDbEnabled;
-    public PluginConfigInfo plugin;
-    public SshdInfo sshd;
-    public SuggestInfo suggest;
-    public Map<String, String> urlAliases;
-    public UserConfigInfo user;
-    public ReceiveInfo receive;
-  }
-
-  public static class AuthInfo {
-    public AuthType authType;
-    public Boolean useContributorAgreements;
-    public List<Account.FieldName> editableAccountFields;
-    public String loginUrl;
-    public String loginText;
-    public String switchAccountUrl;
-    public String registerUrl;
-    public String registerText;
-    public String editFullNameUrl;
-    public String httpPasswordUrl;
-    public Boolean isGitBasicAuth;
-  }
-
-  public static class ChangeConfigInfo {
-    public Boolean allowBlame;
-    public Boolean allowDrafts;
-    public int largeChange;
-    public String replyLabel;
-    public String replyTooltip;
-    public int updateDelay;
-    public Boolean submitWholeTopic;
-  }
-
-  public static class DownloadInfo {
-    public Map<String, DownloadSchemeInfo> schemes;
-    public List<String> archives;
-  }
-
-  public static class DownloadSchemeInfo {
-    public String url;
-    public Boolean isAuthRequired;
-    public Boolean isAuthSupported;
-    public Map<String, String> commands;
-    public Map<String, String> cloneCommands;
-  }
-
-  public static class GerritInfo {
-    public String allProjects;
-    public String allUsers;
-    public Boolean docSearch;
-    public String docUrl;
-    public Boolean editGpgKeys;
-    public String reportBugUrl;
-    public String reportBugText;
-  }
-
-  public static class PluginConfigInfo {
-    public Boolean hasAvatars;
-    public List<String> jsResourcePaths;
-  }
-
-  public static class SshdInfo {
-  }
-
-  public static class SuggestInfo {
-    public int from;
-  }
-
-  public static class UserConfigInfo {
-    public String anonymousCowardName;
-  }
-
-  public static class ReceiveInfo {
-    public Boolean enableSignedPush;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
index 33a458e..f7968c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
@@ -67,7 +67,7 @@
   }
 
   @Override
-  public Object apply(ConfigResource rsrc, Input input)
+  public Response<String> apply(ConfigResource rsrc, Input input)
       throws AuthException, BadRequestException, UnprocessableEntityException {
     if (input == null || input.operation == null) {
       throw new BadRequestException("operation must be specified");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 6811056..b09e0fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Optional;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ChangeKind;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -34,6 +35,7 @@
 import com.google.gerrit.server.change.ChangeKindCache;
 import com.google.gerrit.server.change.PatchSetInserter;
 import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.index.change.ChangeIndexer;
@@ -168,24 +170,69 @@
    * @throws UpdateException
    * @throws RestApiException
    */
-  public void publish(ChangeEdit edit) throws NoSuchChangeException,
-      IOException, OrmException, RestApiException, UpdateException {
+  public void publish(final ChangeEdit edit, NotifyHandling notify)
+      throws NoSuchChangeException, IOException, OrmException, RestApiException,
+      UpdateException {
     Change change = edit.getChange();
     try (Repository repo = gitManager.openRepository(change.getProject());
         RevWalk rw = new RevWalk(repo);
-        ObjectInserter inserter = repo.newObjectInserter()) {
+        ObjectInserter oi = repo.newObjectInserter()) {
       PatchSet basePatchSet = edit.getBasePatchSet();
       if (!basePatchSet.getId().equals(change.currentPatchSetId())) {
         throw new ResourceConflictException(
             "only edit for current patch set can be published");
       }
 
-      Change updatedChange =
-          insertPatchSet(edit, change, repo, rw, inserter, basePatchSet,
-              squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet));
-      // TODO(davido): This should happen in the same BatchRefUpdate.
-      deleteRef(repo, edit);
-      indexer.index(db.get(), updatedChange);
+      RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet);
+      ChangeControl ctl =
+          changeControlFactory.controlFor(db.get(), change, edit.getUser());
+      PatchSet.Id psId =
+          ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
+      PatchSetInserter inserter = patchSetInserterFactory
+          .create(ctl, psId, squashed)
+          .setNotify(notify);
+
+      StringBuilder message = new StringBuilder("Patch Set ")
+        .append(inserter.getPatchSetId().get())
+        .append(": ");
+
+      ProjectState project = projectCache.get(change.getDest().getParentKey());
+      // Previously checked that the base patch set is the current patch set.
+      ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get());
+      ChangeKind kind = changeKindCache.getChangeKind(project, repo, prior, squashed);
+      if (kind == ChangeKind.NO_CODE_CHANGE) {
+        message.append("Commit message was updated.");
+      } else {
+        message.append("Published edit on patch set ")
+          .append(basePatchSet.getPatchSetId())
+          .append(".");
+      }
+
+      try (BatchUpdate bu = updateFactory.create(
+          db.get(), change.getProject(), ctl.getUser(),
+          TimeUtil.nowTs())) {
+        bu.setRepository(repo, rw, oi);
+        bu.addOp(change.getId(), inserter
+          .setDraft(change.getStatus() == Status.DRAFT ||
+              basePatchSet.isDraft())
+          .setMessage(message.toString()));
+        bu.addOp(change.getId(), new BatchUpdate.Op() {
+          @Override
+          public void updateRepo(RepoContext ctx) throws Exception {
+            deleteRef(ctx.getRepository(), edit);
+          }
+        });
+        bu.execute();
+      } catch (UpdateException e) {
+        if (e.getCause() instanceof IOException && e.getMessage()
+            .equals(String.format("%s: Failed to delete ref %s: %s",
+                IOException.class.getName(), edit.getRefName(),
+                RefUpdate.Result.LOCK_FAILURE.name()))) {
+          throw new ResourceConflictException("edit ref was updated");
+        }
+      }
+
+      indexer.index(db.get(), inserter.getChange());
     }
   }
 
@@ -230,47 +277,6 @@
     return writeSquashedCommit(rw, inserter, parent, edit);
   }
 
-  private Change insertPatchSet(ChangeEdit edit, Change change,
-      Repository repo, RevWalk rw, ObjectInserter oi, PatchSet basePatchSet,
-      RevCommit squashed) throws NoSuchChangeException, RestApiException,
-      UpdateException, OrmException, IOException {
-    ChangeControl ctl =
-        changeControlFactory.controlFor(db.get(), change, edit.getUser());
-    PatchSet.Id psId =
-        ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
-    PatchSetInserter inserter =
-        patchSetInserterFactory.create(ctl, psId, squashed);
-
-    StringBuilder message = new StringBuilder("Patch Set ")
-      .append(inserter.getPatchSetId().get())
-      .append(": ");
-
-    ProjectState project = projectCache.get(change.getDest().getParentKey());
-    // Previously checked that the base patch set is the current patch set.
-    ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get());
-    ChangeKind kind = changeKindCache.getChangeKind(project, repo, prior, squashed);
-    if (kind == ChangeKind.NO_CODE_CHANGE) {
-      message.append("Commit message was updated.");
-    } else {
-      message.append("Published edit on patch set ")
-        .append(basePatchSet.getPatchSetId())
-        .append(".");
-    }
-
-    try (BatchUpdate bu = updateFactory.create(
-        db.get(), change.getProject(), ctl.getUser(),
-        TimeUtil.nowTs())) {
-      bu.setRepository(repo, rw, oi);
-      bu.addOp(change.getId(), inserter
-        .setDraft(change.getStatus() == Status.DRAFT ||
-            basePatchSet.isDraft())
-        .setMessage(message.toString()));
-      bu.execute();
-    }
-
-    return inserter.getChange();
-  }
-
   private static void deleteRef(Repository repo, ChangeEdit edit)
       throws IOException {
     String refName = edit.getRefName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 1368bf3..8d093f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -321,7 +321,7 @@
   }
 
   @Override
-  public void onReviewerAdded(ReviewerAddedListener.Event ev) {
+  public void onReviewersAdded(ReviewerAddedListener.Event ev) {
     try {
       ChangeNotes notes = getNotes(ev.getChange());
       Change change = notes.getChange();
@@ -330,9 +330,10 @@
       event.change = changeAttributeSupplier(change);
       event.patchSet = patchSetAttributeSupplier(change,
           psUtil.current(db.get(), notes));
-      event.reviewer = accountAttributeSupplier(ev.getReviewer());
-
-      dispatcher.get().postEvent(change, event);
+      for (AccountInfo reviewer : ev.getReviewers()) {
+        event.reviewer = accountAttributeSupplier(reviewer);
+        dispatcher.get().postEvent(change, event);
+      }
     } catch (OrmException e) {
       log.error("Failed to dispatch event", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index dd49272..c76e76b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -83,22 +83,15 @@
 
   private static class Event extends AbstractRevisionEvent
       implements ChangeAbandonedListener.Event {
-    private final AccountInfo abandoner;
     private final String reason;
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo abandoner,
         String reason, Timestamp when, NotifyHandling notifyHandling) {
       super(change, revision, abandoner, when, notifyHandling);
-      this.abandoner = abandoner;
       this.reason = reason;
     }
 
     @Override
-    public AccountInfo getAbandoner() {
-      return abandoner;
-    }
-
-    @Override
     public String getReason() {
       return reason;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index 94df1d1..378f2b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -81,22 +81,15 @@
 
   private static class Event extends AbstractRevisionEvent
       implements ChangeMergedListener.Event {
-    private final AccountInfo merger;
     private final String newRevisionId;
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo merger,
         String newRevisionId, Timestamp when) {
       super(change, revision, merger, when, NotifyHandling.ALL);
-      this.merger = merger;
       this.newRevisionId = newRevisionId;
     }
 
     @Override
-    public AccountInfo getMerger() {
-      return merger;
-    }
-
-    @Override
     public String getNewRevisionId() {
       return newRevisionId;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index c853609..05e0d21 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -82,22 +82,15 @@
   private static class Event extends AbstractRevisionEvent
       implements ChangeRestoredListener.Event {
 
-    private AccountInfo restorer;
     private String reason;
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo restorer,
         String reason, Timestamp when) {
       super(change, revision, restorer, when, NotifyHandling.ALL);
-      this.restorer = restorer;
       this.reason = reason;
     }
 
     @Override
-    public AccountInfo getRestorer() {
-      return restorer;
-    }
-
-    @Override
     public String getReason() {
       return reason;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index 85ad8b6..8e27ce9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -90,7 +90,6 @@
   private static class Event extends AbstractRevisionEvent
       implements CommentAddedListener.Event {
 
-    private final AccountInfo author;
     private final String comment;
     private final Map<String, ApprovalInfo> approvals;
     private final Map<String, ApprovalInfo> oldApprovals;
@@ -99,18 +98,12 @@
         String comment, Map<String, ApprovalInfo> approvals,
         Map<String, ApprovalInfo> oldApprovals, Timestamp when) {
       super(change, revision, author, when, NotifyHandling.ALL);
-      this.author = author;
       this.comment = comment;
       this.approvals = approvals;
       this.oldApprovals = oldApprovals;
     }
 
     @Override
-    public AccountInfo getAuthor() {
-      return author;
-    }
-
-    @Override
     public String getComment() {
       return comment;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
index 0895cb8..6b8ce3d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
@@ -78,17 +78,10 @@
 
   private static class Event extends AbstractRevisionEvent
       implements DraftPublishedListener.Event {
-    private final AccountInfo publisher;
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo publisher,
         Timestamp when) {
       super(change, revision, publisher, when, NotifyHandling.ALL);
-      this.publisher = publisher;
-    }
-
-    @Override
-    public AccountInfo getPublisher() {
-      return publisher;
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index fe42d02..f18b963 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -81,7 +81,6 @@
   private static class Event extends AbstractChangeEvent
       implements HashtagsEditedListener.Event {
 
-    private AccountInfo editor;
     private Collection<String> updatedHashtags;
     private Collection<String> addedHashtags;
     private Collection<String> removedHashtags;
@@ -89,18 +88,12 @@
     Event(ChangeInfo change, AccountInfo editor, Collection<String> updated,
         Collection<String> added, Collection<String> removed, Timestamp when) {
       super(change, editor, when, NotifyHandling.ALL);
-      this.editor = editor;
       this.updatedHashtags = updated;
       this.addedHashtags = added;
       this.removedHashtags = removed;
     }
 
     @Override
-    public AccountInfo getEditor() {
-      return editor;
-    }
-
-    @Override
     public Collection<String> getHashtags() {
       return updatedHashtags;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index 3ae8135..35b14dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.extensions.events;
 
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -33,6 +35,7 @@
 
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.List;
 
 public class ReviewerAdded {
   private static final Logger log =
@@ -49,29 +52,38 @@
   }
 
   public void fire(ChangeInfo change, RevisionInfo revision,
-      AccountInfo reviewer, AccountInfo adder, Timestamp when) {
+      List<AccountInfo> reviewers, AccountInfo adder, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
-    Event event = new Event(change, revision, reviewer, adder, when);
+    Event event = new Event(change, revision, reviewers, adder, when);
     for (ReviewerAddedListener l : listeners) {
       try {
-        l.onReviewerAdded(event);
+        l.onReviewersAdded(event);
       } catch (Exception e) {
         log.warn("Error in event listener, e");
       }
     }
   }
 
-  public void fire(Change change, PatchSet patchSet, Account account,
+  public void fire(Change change, PatchSet patchSet, List<Account.Id> reviewers,
       Account adder, Timestamp when) {
-    if (!listeners.iterator().hasNext()) {
+    if (!listeners.iterator().hasNext() || reviewers.isEmpty()) {
       return;
     }
+
+    List<AccountInfo> transformed = Lists.transform(reviewers,
+        new Function<Account.Id, AccountInfo>() {
+          @Override
+          public AccountInfo apply(Account.Id account) {
+            return util.accountInfo(account);
+          }
+        });
+
     try {
       fire(util.changeInfo(change),
           util.revisionInfo(change.getProject(), patchSet),
-          util.accountInfo(account),
+          transformed,
           util.accountInfo(adder),
           when);
     } catch (PatchListNotAvailableException | GpgException | IOException
@@ -82,17 +94,17 @@
 
   private static class Event extends AbstractRevisionEvent
       implements ReviewerAddedListener.Event {
-    private final AccountInfo reviewer;
+    private final List<AccountInfo> reviewers;
 
-    Event(ChangeInfo change, RevisionInfo revision, AccountInfo reviewer,
+    Event(ChangeInfo change, RevisionInfo revision, List<AccountInfo> reviewers,
         AccountInfo adder, Timestamp when) {
       super(change, revision, adder, when, NotifyHandling.ALL);
-      this.reviewer = reviewer;
+      this.reviewers = reviewers;
     }
 
     @Override
-    public AccountInfo getReviewer() {
-      return reviewer;
+    public List<AccountInfo> getReviewers() {
+      return reviewers;
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index 270eb35..b519c46 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -53,12 +53,13 @@
   public void fire(ChangeInfo change, RevisionInfo revision,
       AccountInfo reviewer, AccountInfo remover, String message,
       Map<String, ApprovalInfo> newApprovals,
-      Map<String, ApprovalInfo> oldApprovals, Timestamp when) {
+      Map<String, ApprovalInfo> oldApprovals, NotifyHandling notify,
+      Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
     Event event = new Event(change, revision, reviewer, remover, message,
-        newApprovals, oldApprovals, when);
+        newApprovals, oldApprovals, notify, when);
     for (ReviewerDeletedListener listener : listeners) {
       try {
         listener.onReviewerDeleted(event);
@@ -69,9 +70,8 @@
   }
 
   public void fire(Change change, PatchSet patchSet, Account reviewer,
-      Account remover, String message,
-      Map<String, Short> newApprovals,
-      Map<String, Short> oldApprovals, Timestamp when) {
+      Account remover, String message, Map<String, Short> newApprovals,
+      Map<String, Short> oldApprovals, NotifyHandling notify, Timestamp when) {
     if (!listeners.iterator().hasNext()) {
       return;
     }
@@ -83,6 +83,7 @@
           message,
           util.approvals(reviewer, newApprovals, when),
           util.approvals(reviewer, oldApprovals, when),
+          notify,
           when);
     } catch (PatchListNotAvailableException | GpgException | IOException
         | OrmException e) {
@@ -101,8 +102,9 @@
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo reviewer,
         AccountInfo remover, String comment,
         Map<String, ApprovalInfo> newApprovals,
-        Map<String, ApprovalInfo> oldApprovals, Timestamp when) {
-      super(change, revision, remover, when, NotifyHandling.ALL);
+        Map<String, ApprovalInfo> oldApprovals, NotifyHandling notify,
+        Timestamp when) {
+      super(change, revision, remover, when, notify);
       this.reviewer = reviewer;
       this.comment = comment;
       this.newApprovals = newApprovals;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index 98fa05e..71bc9ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -81,17 +81,10 @@
 
   private static class Event extends AbstractRevisionEvent
       implements RevisionCreatedListener.Event {
-    private final AccountInfo uploader;
 
     Event(ChangeInfo change, RevisionInfo revision, AccountInfo uploader,
         Timestamp when, NotifyHandling notify) {
       super(change, revision, uploader, when, notify);
-      this.uploader = uploader;
-    }
-
-    @Override
-    public AccountInfo getUploader() {
-      return uploader;
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index 0a0a8ca..77c1647 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -75,22 +75,15 @@
 
   private static class Event extends AbstractChangeEvent
       implements TopicEditedListener.Event {
-    private final AccountInfo editor;
     private final String oldTopic;
 
     Event(ChangeInfo change, AccountInfo editor, String oldTopic,
         Timestamp when) {
       super(change, editor, when, NotifyHandling.ALL);
-      this.editor = editor;
       this.oldTopic = oldTopic;
     }
 
     @Override
-    public AccountInfo getEditor() {
-      return editor;
-    }
-
-    @Override
     public String getOldTopic() {
       return oldTopic;
     }
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
index 0e954f3..26c59c2 100644
--- 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
@@ -67,7 +67,7 @@
     logDebug("Loading .gitmodules of {} for project {}", branch, project);
     OpenRepo or;
     try {
-      or = orm.openRepo(project, false);
+      or = orm.openRepo(project);
     } catch (NoSuchProjectException e) {
       throw new IOException(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 9d62721..9d4c811 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
@@ -727,7 +727,7 @@
       throws IntegrationException {
     for (Project.NameKey project : projects) {
       try {
-        orm.openRepo(project, true);
+        orm.openRepo(project);
       } catch (NoSuchProjectException noProject) {
         logWarn("Project " + noProject.project() + " no longer exists, "
             + "abandoning open changes");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index fb4c2d4..cd76aff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -187,13 +187,8 @@
     return or;
   }
 
-  public OpenRepo openRepo(Project.NameKey project, boolean abortIfOpen)
+  public OpenRepo openRepo(Project.NameKey project)
       throws NoSuchProjectException, IOException {
-    if (abortIfOpen) {
-      checkState(!openRepos.containsKey(project),
-          "repo already opened: %s", project);
-    }
-
     if (openRepos.containsKey(project)) {
       return openRepos.get(project);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index 284e9ed..315365b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -32,13 +32,13 @@
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -68,7 +68,6 @@
  * If change.submitWholeTopic is enabled, also all changes of the topic
  * and their parents are included.
  */
-@Singleton
 public class MergeSuperSet {
   private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
 
@@ -77,6 +76,7 @@
     for (ChangeData cd : cs.changes()) {
       cd.reloadChange();
       cd.setPatchSets(null);
+      cd.setMergeable(null);
     }
   }
 
@@ -261,11 +261,19 @@
         continue;
       }
       for (ChangeData topicCd : query().byTopicOpen(topic)) {
-        topicCd.changeControl(user);
-        if (topicCd.changeControl().isVisible(db, topicCd)) {
-          visibleChanges.add(topicCd);
-        } else {
-          nonVisibleChanges.add(topicCd);
+        try {
+          topicCd.changeControl(user);
+          if (topicCd.changeControl().isVisible(db, topicCd)) {
+            visibleChanges.add(topicCd);
+          } else {
+            nonVisibleChanges.add(topicCd);
+          }
+        } catch (OrmException e) {
+          if (e.getCause() instanceof NoSuchChangeException) {
+            // Ignore and skip this change
+          } else {
+            throw e;
+          }
         }
       }
       topicsSeen.add(topic);
@@ -307,13 +315,15 @@
   }
 
   private InternalChangeQuery query() {
-    // Request fields required for completing the ChangeSet without having to
-    // touch the database. This provides reasonable performance when loading the
-    // change screen; callers that care about reading the latest value of these
-    // fields should clear them explicitly using reloadChanges().
+    // Request fields required for completing the ChangeSet and converting to
+    // ChangeInfo without having to touch the database or opening the repository
+    // more than necessary. This provides reasonable performance when loading
+    // the change screen; callers that care about reading the latest value of
+    // these fields should clear them explicitly using reloadChanges().
     Set<String> fields = ImmutableSet.of(
         ChangeField.CHANGE.getName(),
-        ChangeField.PATCH_SET.getName());
+        ChangeField.PATCH_SET.getName(),
+        ChangeField.MERGEABLE.getName());
     return queryProvider.get().setRequestedFields(fields);
   }
 
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 1721ae2..1429079 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
@@ -91,7 +91,7 @@
   private static final String PROJECT = "project";
   private static final String KEY_DESCRIPTION = "description";
 
-  private static final String ACCESS = "access";
+  public static final String ACCESS = "access";
   private static final String KEY_INHERIT_FROM = "inheritFrom";
   private static final String KEY_GROUP_PERMISSIONS = "exclusiveGroupPermissions";
 
@@ -151,10 +151,13 @@
   private static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange";
   private static final String KEY_VALUE = "value";
   private static final String KEY_CAN_OVERRIDE = "canOverride";
-  private static final String KEY_Branch = "branch";
+  private static final String KEY_BRANCH = "branch";
   private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of(
       "MaxWithBlock", "AnyWithBlock", "MaxNoBlock", "NoBlock", "NoOp", "PatchSetLock");
 
+  private static final String LEGACY_PERMISSION_PUSH_TAG = "pushTag";
+  private static final String LEGACY_PERMISSION_PUSH_SIGNED_TAG = "pushSignedTag";
+
   private static final String PLUGIN = "plugin";
 
   private static final SubmitType defaultSubmitAction =
@@ -180,6 +183,7 @@
   private Map<String, Config> pluginConfigs;
   private boolean checkReceivedObjects;
   private Set<String> sectionsWithUnknownPermissions;
+  private boolean hasLegacyPermissions;
 
   public static ProjectConfig read(MetaDataUpdate update) throws IOException,
       ConfigInvalidException {
@@ -627,6 +631,7 @@
 
         for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
           for (String n : varName.split("[, \t]{1,}")) {
+            n = convertLegacyPermission(n);
             if (isPermission(n)) {
               as.getPermission(n, true).setExclusiveGroup(true);
             }
@@ -634,10 +639,11 @@
         }
 
         for (String varName : rc.getNames(ACCESS, refName)) {
-          if (isPermission(varName)) {
-            Permission perm = as.getPermission(varName, true);
+          String convertedName = convertLegacyPermission(varName);
+          if (isPermission(convertedName)) {
+            Permission perm = as.getPermission(convertedName, true);
             loadPermissionRules(rc, ACCESS, refName, varName, groupsByName,
-                perm, Permission.hasRange(varName));
+                perm, Permission.hasRange(convertedName));
           } else {
             sectionsWithUnknownPermissions.add(as.getName());
           }
@@ -805,7 +811,7 @@
       label.setCanOverride(
           rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE,
               LabelType.DEF_CAN_OVERRIDE));
-      label.setRefPatterns(getStringListOrNull(rc, LABEL, name, KEY_Branch));
+      label.setRefPatterns(getStringListOrNull(rc, LABEL, name, KEY_BRANCH));
       labelSections.put(name, label);
     }
   }
@@ -1147,7 +1153,8 @@
       }
 
       for (String varName : rc.getNames(ACCESS, refName)) {
-        if (isPermission(varName) && !have.contains(varName.toLowerCase())) {
+        if (isPermission(convertLegacyPermission(varName))
+            && !have.contains(varName.toLowerCase())) {
           rc.unset(ACCESS, refName, varName);
         }
       }
@@ -1282,4 +1289,21 @@
     Collections.sort(r);
     return r;
   }
+
+  public boolean hasLegacyPermissions() {
+    return hasLegacyPermissions;
+  }
+
+  private String convertLegacyPermission(String permissionName) {
+    switch(permissionName) {
+      case LEGACY_PERMISSION_PUSH_TAG:
+        hasLegacyPermissions = true;
+        return Permission.CREATE_TAG;
+      case LEGACY_PERMISSION_PUSH_SIGNED_TAG:
+        hasLegacyPermissions = true;
+        return Permission.CREATE_SIGNED_TAG;
+      default:
+        return permissionName;
+    }
+  }
 }
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 7fadae0..53e8ee8 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
@@ -42,6 +42,7 @@
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
@@ -315,6 +316,8 @@
   private final RequestId receiveId;
   private MagicBranchInput magicBranch;
   private boolean newChangeForAllNotInTarget;
+  private final ListMultimap<String, String> pushOptions =
+      LinkedListMultimap.create();
 
   private List<CreateRequest> newChanges = Collections.emptyList();
   private final Map<Change.Id, ReplaceRequest> replaceByChange =
@@ -490,6 +493,7 @@
     advHooks.add(new HackPushNegotiateHook());
     rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
     rp.setPostReceiveHook(lazyPostReceive.get());
+    rp.setAllowPushOptions(true);
   }
 
   public void init() {
@@ -915,6 +919,18 @@
   }
 
   private void parseCommands(Collection<ReceiveCommand> commands) {
+    List<String> optionList = rp.getPushOptions();
+    if (optionList != null) {
+      for (String option : optionList) {
+        int e = option.indexOf('=');
+        if (e > 0) {
+          pushOptions.put(option.substring(0, e), option.substring(e + 1));
+        } else {
+          pushOptions.put(option, "");
+        }
+      }
+    }
+
     logDebug("Parsing {} commands", commands.size());
     for (ReceiveCommand cmd : commands) {
       if (cmd.getResult() != NOT_ATTEMPTED) {
@@ -1305,14 +1321,14 @@
       return new MailRecipients(reviewer, cc);
     }
 
-    String parse(CmdLineParser clp, Repository repo, Set<String> refs)
-        throws CmdLineException {
+    String parse(CmdLineParser clp, Repository repo, Set<String> refs,
+        ListMultimap<String, String> pushOptions) throws CmdLineException {
       String ref = RefNames.fullName(
           MagicBranch.getDestBranchName(cmd.getRefName()));
 
+      ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions);
       int optionStart = ref.indexOf('%');
       if (0 < optionStart) {
-        ListMultimap<String, String> options = LinkedListMultimap.create();
         for (String s : COMMAS.split(ref.substring(optionStart + 1))) {
           int e = s.indexOf('=');
           if (0 < e) {
@@ -1321,10 +1337,13 @@
             options.put(s, "");
           }
         }
-        clp.parseOptionMap(options);
         ref = ref.substring(0, optionStart);
       }
 
+      if (!options.isEmpty()) {
+        clp.parseOptionMap(options);
+      }
+
       // Split the destination branch by branch and topic. The topic
       // suffix is entirely optional, so it might not even exist.
       String head = readHEAD(repo);
@@ -1347,6 +1366,19 @@
     }
   }
 
+  /**
+   * Gets an unmodifiable view of the pushOptions.
+   * <p>
+   * The collection is empty if the client does not support push options, or if
+   * the client did not send any options.
+   *
+   * @return an unmodifiable view of pushOptions.
+   */
+  @Nullable
+  public ListMultimap<String, String> getPushOptions() {
+    return ImmutableListMultimap.copyOf(pushOptions);
+  }
+
   private void parseMagicBranch(ReceiveCommand cmd) {
     // Permit exactly one new change request per push.
     if (magicBranch != null) {
@@ -1362,8 +1394,10 @@
     String ref;
     CmdLineParser clp = optionParserFactory.create(magicBranch);
     magicBranch.clp = clp;
+
     try {
-      ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet());
+      ref = magicBranch.parse(
+          clp, repo, rp.getAdvertisedRefs().keySet(), pushOptions);
     } catch (CmdLineException e) {
       if (!clp.wasHelpRequestedByOption()) {
         logDebug("Invalid branch syntax");
@@ -1428,7 +1462,7 @@
     }
 
     if (magicBranch.submit && !projectControl.controlForRef(
-        MagicBranch.NEW_CHANGE + ref).canSubmit()) {
+        MagicBranch.NEW_CHANGE + ref).canSubmit(true)) {
       reject(cmd, "submit not allowed");
       return;
     }
@@ -1655,9 +1689,7 @@
         Collection<Ref> existingRefs = existing.get(c);
 
         if (rejectImplicitMerges) {
-          for (RevCommit p : c.getParents()) {
-            mergedParents.add(p);
-          }
+          Collections.addAll(mergedParents, c.getParents());
           mergedParents.remove(c);
         }
 
@@ -1855,7 +1887,7 @@
   }
 
   private void rejectImplicitMerges(Set<RevCommit> mergedParents)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException {
+      throws IOException {
     if (!mergedParents.isEmpty()) {
       Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
       if (targetRef != null) {
@@ -2556,12 +2588,11 @@
     rw.parseBody(c);
     CommitReceivedEvent receiveEvent =
         new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, user);
-    CommitValidators commitValidators =
-        commitValidatorsFactory.create(ctl, sshInfo, repo);
+    CommitValidators commitValidators = commitValidatorsFactory.create(
+        CommitValidators.Policy.RECEIVE_COMMITS, ctl, sshInfo, repo);
 
     try {
-      messages.addAll(commitValidators.validateForReceiveCommits(
-          receiveEvent, rejectCommits));
+      messages.addAll(commitValidators.validate(receiveEvent));
     } catch (CommitValidationException e) {
       logDebug("Commit validation failed on {}", c.name());
       messages.addAll(e.getMessages());
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 fe568c8..a70fa7a 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
@@ -255,13 +255,15 @@
     ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getControl());
     MailRecipients oldRecipients =
         getRecipientsFromReviewers(cd.reviewers());
-    approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet);
+    Iterable<PatchSetApproval> newApprovals =
+        approvalsUtil.addApprovals(ctx.getDb(), update,
+            projectControl.getLabelTypes(), newPatchSet, ctx.getControl(),
+            approvals);
+    approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet,
+        newApprovals);
     approvalsUtil.addReviewers(ctx.getDb(), update,
         projectControl.getLabelTypes(), change, newPatchSet, info,
         recipients.getReviewers(), oldRecipients.getAll());
-    approvalsUtil.addApprovals(ctx.getDb(), update,
-        projectControl.getLabelTypes(), newPatchSet, ctx.getControl(),
-        approvals);
     recipients.add(oldRecipients);
 
     String approvalMessage = ApprovalsUtil.renderMessageWithApprovals(
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 01d73ec..8e84ffb 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
@@ -251,7 +251,7 @@
       }
       OpenRepo or;
       try {
-        or = orm.openRepo(s.getProject(), false);
+        or = orm.openRepo(s.getProject());
       } catch (NoSuchProjectException e) {
         // A project listed a non existent project to be allowed
         // to subscribe to it. Allow this for now, i.e. no exception is
@@ -290,7 +290,7 @@
       for (Branch.NameKey targetBranch : branches) {
         Project.NameKey targetProject = targetBranch.getParentKey();
         try {
-          OpenRepo or = orm.openRepo(targetProject, false);
+          OpenRepo or = orm.openRepo(targetProject);
           ObjectId id = or.repo.resolve(targetBranch.get());
           if (id == null) {
             logDebug("The branch " + targetBranch + " doesn't exist.");
@@ -327,7 +327,7 @@
         if (dst.containsKey(project)) {
           superProjects.add(project);
           // get a new BatchUpdate for the super project
-          OpenRepo or = orm.openRepo(project, false);
+          OpenRepo or = orm.openRepo(project);
           for (Branch.NameKey branch : dst.get(project)) {
             addOp(or.getUpdate(), branch);
           }
@@ -348,7 +348,7 @@
       throws IOException, SubmoduleException {
     OpenRepo or;
     try {
-      or = orm.openRepo(subscriber.getParentKey(), false);
+      or = orm.openRepo(subscriber.getParentKey());
     } catch (NoSuchProjectException | IOException e) {
       throw new SubmoduleException("Cannot access superproject", e);
     }
@@ -404,7 +404,7 @@
       throws IOException, SubmoduleException {
     OpenRepo or;
     try {
-      or = orm.openRepo(subscriber.getParentKey(), false);
+      or = orm.openRepo(subscriber.getParentKey());
     } catch (NoSuchProjectException | IOException e) {
       throw new SubmoduleException("Cannot access superproject", e);
     }
@@ -444,7 +444,7 @@
       throws SubmoduleException, IOException {
     OpenRepo subOr;
     try {
-      subOr = orm.openRepo(s.getSubmodule().getParentKey(), false);
+      subOr = orm.openRepo(s.getSubmodule().getParentKey());
     } catch (NoSuchProjectException | IOException e) {
       throw new SubmoduleException("Cannot access submodule", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index d4956ab..4e44a6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -16,8 +16,10 @@
 
 import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
+import static com.google.gerrit.server.git.ReceiveCommits.NEW_PATCHSET;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
@@ -31,15 +33,15 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
+import com.google.inject.Singleton;
 
 import com.jcraft.jsch.HostKey;
 
@@ -51,6 +53,7 @@
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.SystemReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -68,107 +71,99 @@
       .getLogger(CommitValidators.class);
 
   public enum Policy {
-    /** Use {@link #validateForGerritCommits}. */
+    /** Use {@link Factory#forGerritCommits}. */
     GERRIT,
 
-    /** Use {@link #validateForReceiveCommits}. */
+    /** Use {@link Factory#forReceiveCommits}. */
     RECEIVE_COMMITS,
 
     /** Do not validate commits. */
     NONE
   }
 
-  public interface Factory {
-    CommitValidators create(RefControl refControl, SshInfo sshInfo,
-        Repository repo);
-  }
+  @Singleton
+  public static class Factory {
+    private final PersonIdent gerritIdent;
+    private final String canonicalWebUrl;
+    private final DynamicSet<CommitValidationListener> pluginValidators;
+    private final AllUsersName allUsers;
+    private final String installCommitMsgHookCommand;
 
-  private final PersonIdent gerritIdent;
-  private final RefControl refControl;
-  private final String canonicalWebUrl;
-  private final String installCommitMsgHookCommand;
-  private final SshInfo sshInfo;
-  private final Repository repo;
-  private final DynamicSet<CommitValidationListener> commitValidationListeners;
-  private final AllUsersName allUsers;
-
-  @Inject
-  CommitValidators(@GerritPersonIdent PersonIdent gerritIdent,
-      @CanonicalWebUrl @Nullable String canonicalWebUrl,
-      @GerritServerConfig Config config,
-      DynamicSet<CommitValidationListener> commitValidationListeners,
-      AllUsersName allUsers,
-      @Assisted SshInfo sshInfo,
-      @Assisted Repository repo,
-      @Assisted RefControl refControl) {
-    this.gerritIdent = gerritIdent;
-    this.canonicalWebUrl = canonicalWebUrl;
-    this.installCommitMsgHookCommand =
-        config.getString("gerrit", null, "installCommitMsgHookCommand");
-    this.commitValidationListeners = commitValidationListeners;
-    this.allUsers = allUsers;
-    this.sshInfo = sshInfo;
-    this.repo = repo;
-    this.refControl = refControl;
-  }
-
-  public List<CommitValidationMessage> validateForReceiveCommits(
-      CommitReceivedEvent receiveEvent, NoteMap rejectCommits)
-      throws CommitValidationException {
-
-    List<CommitValidationListener> validators = new LinkedList<>();
-
-    validators.add(new UploadMergesPermissionValidator(refControl));
-    validators.add(new AmendedGerritMergeCommitValidationListener(
-        refControl, gerritIdent));
-    validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl));
-    validators.add(new CommitterUploaderValidator(refControl, canonicalWebUrl));
-    validators.add(new SignedOffByValidator(refControl));
-    if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
-        || ReceiveCommits.NEW_PATCHSET.matcher(
-            receiveEvent.command.getRefName()).matches()) {
-      validators.add(new ChangeIdValidator(refControl, canonicalWebUrl,
-          installCommitMsgHookCommand, sshInfo));
+    @Inject
+    Factory(@GerritPersonIdent PersonIdent gerritIdent,
+        @CanonicalWebUrl @Nullable String canonicalWebUrl,
+        @GerritServerConfig Config cfg,
+        DynamicSet<CommitValidationListener> pluginValidators,
+        AllUsersName allUsers) {
+      this.gerritIdent = gerritIdent;
+      this.canonicalWebUrl = canonicalWebUrl;
+      this.pluginValidators = pluginValidators;
+      this.allUsers = allUsers;
+      this.installCommitMsgHookCommand = cfg != null
+          ? cfg.getString("gerrit", null, "installCommitMsgHookCommand") : null;
     }
-    validators.add(new ConfigValidator(refControl, repo, allUsers));
-    validators.add(new BannedCommitsValidator(rejectCommits));
-    validators.add(new PluginCommitValidationListener(commitValidationListeners));
 
-    List<CommitValidationMessage> messages = new LinkedList<>();
-
-    try {
-      for (CommitValidationListener commitValidator : validators) {
-        messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+    public CommitValidators create(Policy policy, RefControl refControl,
+        SshInfo sshInfo, Repository repo) throws IOException {
+      switch (policy) {
+        case RECEIVE_COMMITS:
+          return forReceiveCommits(refControl, sshInfo, repo);
+        case GERRIT:
+          return forGerritCommits(refControl, sshInfo, repo);
+        case NONE:
+          return none();
+        default:
+          throw new IllegalArgumentException("unspported policy: " + policy);
       }
-    } catch (CommitValidationException e) {
-      // Keep the old messages (and their order) in case of an exception
-      messages.addAll(e.getMessages());
-      throw new CommitValidationException(e.getMessage(), messages);
     }
-    return messages;
+
+    private CommitValidators forReceiveCommits(RefControl refControl,
+        SshInfo sshInfo, Repository repo) throws IOException {
+      try (RevWalk rw = new RevWalk(repo)) {
+        NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(repo, rw);
+        return new CommitValidators(ImmutableList.of(
+            new UploadMergesPermissionValidator(refControl),
+            new AmendedGerritMergeCommitValidationListener(
+                refControl, gerritIdent),
+            new AuthorUploaderValidator(refControl, canonicalWebUrl),
+            new CommitterUploaderValidator(refControl, canonicalWebUrl),
+            new SignedOffByValidator(refControl),
+            new ChangeIdValidator(refControl, canonicalWebUrl,
+                installCommitMsgHookCommand, sshInfo),
+            new ConfigValidator(refControl, repo, allUsers),
+            new BannedCommitsValidator(rejectCommits),
+            new PluginCommitValidationListener(pluginValidators)));
+      }
+    }
+
+    private CommitValidators forGerritCommits(RefControl refControl,
+        SshInfo sshInfo, Repository repo) {
+      return new CommitValidators(ImmutableList.of(
+          new UploadMergesPermissionValidator(refControl),
+          new AmendedGerritMergeCommitValidationListener(
+              refControl, gerritIdent),
+          new AuthorUploaderValidator(refControl, canonicalWebUrl),
+          new SignedOffByValidator(refControl),
+          new ChangeIdValidator(refControl, canonicalWebUrl,
+                installCommitMsgHookCommand, sshInfo),
+          new ConfigValidator(refControl, repo, allUsers),
+          new PluginCommitValidationListener(pluginValidators)));
+    }
+
+    private CommitValidators none() {
+      return new CommitValidators(ImmutableList.<CommitValidationListener>of());
+    }
   }
 
-  public List<CommitValidationMessage> validateForGerritCommits(
+  private final List<CommitValidationListener> validators;
+
+  CommitValidators(List<CommitValidationListener> validators) {
+    this.validators = validators;
+  }
+
+  public List<CommitValidationMessage> validate(
       CommitReceivedEvent receiveEvent) throws CommitValidationException {
-
-    List<CommitValidationListener> validators = new LinkedList<>();
-
-    validators.add(new UploadMergesPermissionValidator(refControl));
-    validators.add(new AmendedGerritMergeCommitValidationListener(
-        refControl, gerritIdent));
-    validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl));
-    validators.add(new SignedOffByValidator(refControl));
-    if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
-        || ReceiveCommits.NEW_PATCHSET.matcher(
-            receiveEvent.command.getRefName()).matches()) {
-      validators.add(new ChangeIdValidator(refControl, canonicalWebUrl,
-          installCommitMsgHookCommand, sshInfo));
-    }
-    validators.add(new ConfigValidator(refControl, repo, allUsers));
-    validators.add(new PluginCommitValidationListener(commitValidationListeners));
-
     List<CommitValidationMessage> messages = new LinkedList<>();
-
     try {
       for (CommitValidationListener commitValidator : validators) {
         messages.addAll(commitValidator.onCommitReceived(receiveEvent));
@@ -221,6 +216,9 @@
     @Override
     public List<CommitValidationMessage> onCommitReceived(
         CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      if (!shouldValidateChangeId(receiveEvent)) {
+        return Collections.emptyList();
+      }
       RevCommit commit = receiveEvent.commit;
       List<CommitValidationMessage> messages = new LinkedList<>();
       List<String> idList = commit.getFooterLines(FooterConstants.CHANGE_ID);
@@ -255,6 +253,11 @@
       return Collections.emptyList();
     }
 
+    private static boolean shouldValidateChangeId(CommitReceivedEvent event) {
+      return MagicBranch.isMagicBranch(event.command.getRefName())
+          || NEW_PATCHSET.matcher(event.command.getRefName()).matches();
+    }
+
     private CommitValidationMessage getMissingChangeIdErrorMsg(
         final String errMsg, final RevCommit c) {
       StringBuilder sb = new StringBuilder();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index bd74fff..c10b279 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -27,7 +28,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index 1e8bdf4..eafa74d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -50,6 +50,6 @@
 
   @Override
   protected void formatChange() throws EmailException {
-    appendText(velocifyFile("Abandoned.vm"));
+    appendText(textTemplate("Abandoned"));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
index f825d1c..61ef92d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
@@ -80,7 +80,8 @@
 
   @Override
   protected void format() throws EmailException {
-    appendText(velocifyFile("AddKey.vm"));
+    appendText(textTemplate("AddKey"));
+    appendHtml(soyHtmlTemplate("AddKeyHtml"));
   }
 
   public String getEmail() {
@@ -110,4 +111,19 @@
     }
     return null;
   }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData.put("email", getEmail());
+    soyContextEmailData.put("gpgKeys", getGpgKeys());
+    soyContextEmailData.put("keyType", getKeyType());
+    soyContextEmailData.put("sshKey", getSshKey());
+    soyContextEmailData.put("userNameEmail", getUserNameEmail());
+  }
+
+  @Override
+  protected boolean useHtml() {
+    return true;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index badc706..b310feb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -55,6 +55,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -120,7 +121,7 @@
   @Override
   protected void format() throws EmailException {
     formatChange();
-    appendText(velocifyFile("ChangeFooter.vm"));
+    appendText(textTemplate("ChangeFooter"));
     try {
       TreeSet<String> names = new TreeSet<>();
       for (Account.Id who : changeData.reviewers().all()) {
@@ -199,7 +200,7 @@
   }
 
   private void setChangeSubjectHeader() throws EmailException {
-    setHeader("Subject", velocifyFile("ChangeSubject.vm"));
+    setHeader("Subject", textTemplate("ChangeSubject"));
   }
 
   /** Get a link to the change; null if the server doesn't know its own address. */
@@ -435,11 +436,48 @@
     velocityContext.put("patchSetInfo", patchSetInfo);
   }
 
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+
+    soyContext.put("changeId", change.getKey().get());
+    soyContext.put("coverLetter", getCoverLetter());
+    soyContext.put("fromName", getNameFor(fromId));
+
+    soyContextEmailData.put("unifiedDiff", getUnifiedDiff());
+    soyContextEmailData.put("changeDetail", getChangeDetail());
+    soyContextEmailData.put("changeUrl", getChangeUrl());
+    soyContextEmailData.put("includeDiff", getIncludeDiff());
+
+    Map<String, String> changeData = new HashMap<>();
+    changeData.put("subject", change.getSubject());
+    changeData.put("originalSubject", change.getOriginalSubject());
+    changeData.put("ownerEmail", getNameEmailFor(change.getOwner()));
+    soyContext.put("change", changeData);
+
+    String subject = change.getSubject();
+    changeData.put("subject", subject);
+    // shortSubject is the subject limited to 63 characters, with an ellipsis if
+    // it exceeds that.
+    if (subject.length() < 64) {
+      changeData.put("shortSubject", subject);
+    } else {
+      changeData.put("shortSubject", subject.substring(0, 60) + "...");
+    }
+
+    Map<String, Object> patchSetData = new HashMap<>();
+    patchSetData.put("patchSetId", patchSet.getPatchSetId());
+    patchSetData.put("refName", patchSet.getRefName());
+    soyContext.put("patchSet", patchSetData);
+
+    // TODO(wyatta): patchSetInfo
+  }
+
   public boolean getIncludeDiff() {
     return args.settings.includeDiff;
   }
 
-  private static int HEAP_EST_SIZE = 32 * 1024;
+  private static final int HEAP_EST_SIZE = 32 * 1024;
 
   /** Show patch set as unified difference. */
   public String getUnifiedDiff() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index b56b737..2424a6d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -97,12 +97,12 @@
 
   @Override
   public void formatChange() throws EmailException {
-    appendText(velocifyFile("Comment.vm"));
+    appendText(textTemplate("Comment"));
   }
 
   @Override
   public void formatFooter() throws EmailException {
-    appendText(velocifyFile("CommentFooter.vm"));
+    appendText(textTemplate("CommentFooter"));
   }
 
   public boolean hasInlineComments() {
@@ -283,4 +283,11 @@
       return null;
     }
   }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData.put("inlineComments", getInlineComments());
+    soyContextEmailData.put("hasInlineComments", hasInlineComments());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java
index 75f9f82..8a26a10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java
@@ -65,7 +65,7 @@
 
   @Override
   protected void formatChange() throws EmailException {
-    appendText(velocifyFile("DeleteReviewer.vm"));
+    appendText(textTemplate("DeleteReviewer"));
   }
 
   public List<String> getReviewerNames() {
@@ -78,4 +78,10 @@
     }
     return names;
   }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData.put("reviewerNames", getReviewerNames());
+  }
 }
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
index d861109..66f8123 100644
--- 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
@@ -49,6 +49,6 @@
 
   @Override
   protected void formatChange() throws EmailException {
-    appendText(velocifyFile("DeleteVote.vm"));
+    appendText(textTemplate("DeleteVote"));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 68e5e50..71c294b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 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.
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.index.account.AccountIndexCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -43,6 +44,7 @@
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.template.soy.tofu.SoyTofu;
 
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -69,11 +71,13 @@
   final Provider<String> urlProvider;
   final AllProjectsName allProjectsName;
   final List<String> sshAddresses;
+  final SitePaths site;
 
   final ChangeQueryBuilder queryBuilder;
   final Provider<ReviewDb> db;
   final ChangeData.Factory changeDataFactory;
   final RuntimeInstance velocityRuntime;
+  final SoyTofu soyTofu;
   final EmailSettings settings;
   final DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners;
   final StarredChangesUtil starredChangesUtil;
@@ -100,8 +104,10 @@
       Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       RuntimeInstance velocityRuntime,
+      @MailTemplates SoyTofu soyTofu,
       EmailSettings settings,
       @SshAdvertisedAddresses List<String> sshAddresses,
+      SitePaths site,
       DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners,
       StarredChangesUtil starredChangesUtil,
       AccountIndexCollection accountIndexes,
@@ -128,8 +134,10 @@
     this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.velocityRuntime = velocityRuntime;
+    this.soyTofu = soyTofu;
     this.settings = settings;
     this.sshAddresses = sshAddresses;
+    this.site = site;
     this.outgoingEmailValidationListeners = outgoingEmailValidationListeners;
     this.starredChangesUtil = starredChangesUtil;
     this.accountIndexes = accountIndexes;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
index 51f7ad1..0bc65bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -32,6 +32,7 @@
 
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.regex.Pattern;
 
 /** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */
 @Singleton
@@ -53,13 +54,15 @@
       generator =
           new PatternGen(srvAddr, accountCache, anonymousCowardName, name,
               srvAddr.email);
-
     } else if ("USER".equalsIgnoreCase(from)) {
-      generator = new UserGen(accountCache, srvAddr);
-
+      String[] domains = cfg.getStringList("sendemail", null, "allowedDomain");
+      Pattern domainPattern = MailUtil.glob(domains);
+      ParameterizedString namePattern =
+          new ParameterizedString("${user} (Code Review)");
+      generator = new UserGen(accountCache, domainPattern, anonymousCowardName,
+          namePattern, srvAddr);
     } else if ("SERVER".equalsIgnoreCase(from)) {
       generator = new ServerGen(srvAddr);
-
     } else {
       final Address a = Address.parse(from);
       final ParameterizedString name = a.name != null ? new ParameterizedString(a.name) : null;
@@ -84,11 +87,31 @@
 
   static final class UserGen implements FromAddressGenerator {
     private final AccountCache accountCache;
-    private final Address srvAddr;
+    private final Pattern domainPattern;
+    private final String anonymousCowardName;
+    private final ParameterizedString nameRewriteTmpl;
+    private final Address serverAddress;
 
-    UserGen(AccountCache accountCache, Address srvAddr) {
+    /**
+     * From address generator for USER mode
+     *
+     * @param accountCache get user account from id
+     * @param domainPattern allowed user domain pattern that Gerrit can send as
+     *        the user
+     * @param anonymousCowardName name used when user's full name is missing
+     * @param nameRewriteTmpl name template used for rewriting the sender's name
+     *        when Gerrit can not send as the user
+     * @param serverAddress serverAddress.name is used when fromId is null and
+     *        serverAddress.email is used when Gerrit can not send as the user
+     */
+    UserGen(AccountCache accountCache, Pattern domainPattern,
+        String anonymousCowardName, ParameterizedString nameRewriteTmpl,
+        Address serverAddress) {
       this.accountCache = accountCache;
-      this.srvAddr = srvAddr;
+      this.domainPattern = domainPattern;
+      this.anonymousCowardName = anonymousCowardName;
+      this.nameRewriteTmpl = nameRewriteTmpl;
+      this.serverAddress = serverAddress;
     }
 
     @Override
@@ -98,14 +121,44 @@
 
     @Override
     public Address from(final Account.Id fromId) {
+      String senderName;
       if (fromId != null) {
         Account a = accountCache.get(fromId).getAccount();
+        String fullName = a.getFullName();
         String userEmail = a.getPreferredEmail();
-        return new Address(
-            a.getFullName(),
-            userEmail != null ? userEmail : srvAddr.getEmail());
+        if (canRelay(userEmail)) {
+          return new Address(fullName, userEmail);
+        }
+
+        if (fullName == null || "".equals(fullName.trim())) {
+          fullName = anonymousCowardName;
+        }
+        senderName = nameRewriteTmpl.replace("user", fullName).toString();
+      } else {
+        senderName = serverAddress.name;
       }
-      return srvAddr;
+
+      String senderEmail;
+      ParameterizedString senderEmailPattern =
+          new ParameterizedString(serverAddress.email);
+      if (senderEmailPattern.getParameterNames().isEmpty()) {
+        senderEmail = senderEmailPattern.getRawPattern();
+      } else {
+        senderEmail = senderEmailPattern.replace("userHash", hashOf(senderName))
+            .toString();
+      }
+      return new Address(senderName, senderEmail);
+    }
+
+    /** check if Gerrit is allowed to send from {@code userEmail}. */
+    private boolean canRelay(String userEmail) {
+      if (userEmail != null) {
+        int index = userEmail.indexOf('@');
+        if (index > 0 && index < userEmail.length() - 1) {
+          return domainPattern.matcher(userEmail.substring(index + 1)).matches();
+        }
+      }
+      return false;
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java
new file mode 100644
index 0000000..5e0d8b6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java
@@ -0,0 +1,94 @@
+// 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.common.io.CharStreams;
+import com.google.common.io.Resources;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
+import com.google.template.soy.SoyFileSet;
+import com.google.template.soy.tofu.SoyTofu;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** Configures Soy Tofu object for rendering email templates. */
+@Singleton
+public class MailSoyTofuProvider implements Provider<SoyTofu> {
+
+  // Note: will fail to construct the tofu object if this array is empty.
+  private static final String[] TEMPLATES = {
+    "Abandoned.soy",
+    "AddKey.soy",
+    "AddKeyHtml.soy",
+    "ChangeSubject.soy",
+    "ChangeFooter.soy",
+    "Comment.soy",
+    "CommentFooter.soy",
+    "DeleteReviewer.soy",
+    "DeleteVote.soy",
+    "Footer.soy",
+    "FooterHtml.soy",
+    "Merged.soy",
+    "NewChange.soy",
+    "RegisterNewEmail.soy",
+    "ReplacePatchSet.soy",
+    "Restored.soy",
+    "Reverted.soy",
+  };
+
+  private final SitePaths site;
+
+  @Inject
+  MailSoyTofuProvider(SitePaths site) {
+    this.site = site;
+  }
+
+  @Override
+  public SoyTofu get() throws ProvisionException {
+    SoyFileSet.Builder builder = SoyFileSet.builder();
+    for (String name : TEMPLATES) {
+      addTemplate(builder, name);
+    }
+    return builder.build().compileToTofu();
+  }
+
+  private void addTemplate(SoyFileSet.Builder builder, String name)
+      throws ProvisionException {
+    // Load as a file in the mail templates directory if present.
+    Path tmpl = site.mail_dir.resolve(name);
+    if (Files.isRegularFile(tmpl)) {
+      String content;
+      try (Reader r = Files.newBufferedReader(tmpl, StandardCharsets.UTF_8)) {
+        content = CharStreams.toString(r);
+      } catch (IOException err) {
+        throw new ProvisionException("Failed to read template file " +
+            tmpl.toAbsolutePath().toString(), err);
+      }
+      builder.add(content, tmpl.toAbsolutePath().toString());
+      return;
+    }
+
+    // Otherwise load the template as a resource.
+    String resourcePath = "com/google/gerrit/server/mail/" + name;
+    builder.add(Resources.getResource(resourcePath));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailTemplates.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailTemplates.java
new file mode 100644
index 0000000..72fdaae
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailTemplates.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface MailTemplates {}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
index 048a4a4..8a132cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -32,6 +32,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 public class MailUtil {
   public static MailRecipients getRecipientsFromFooters(
@@ -124,4 +125,19 @@
       return Collections.unmodifiableSet(all);
     }
   }
+
+  /** allow wildcard matching for {@code domains} */
+  public static Pattern glob(String[] domains) {
+    // if domains is not set, match anything
+    if (domains == null || domains.length == 0) {
+      return Pattern.compile(".*");
+    }
+
+    StringBuilder sb = new StringBuilder("");
+    for (String domain : domains) {
+      String quoted = "\\Q" + domain.replace("\\E", "\\E\\\\E\\Q") + "\\E|";
+      sb.append(quoted.replace("*", "\\E.*\\Q"));
+    }
+    return Pattern.compile(sb.substring(0, sb.length() - 1));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index f6c3d0f..c2a3cdd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -58,7 +58,7 @@
 
   @Override
   protected void formatChange() throws EmailException {
-    appendText(velocifyFile("Merged.vm"));
+    appendText(textTemplate("Merged"));
   }
 
   public String getApprovals() {
@@ -123,4 +123,10 @@
     txt.append('\n');
     return txt.toString();
   }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData.put("approvals", getApprovals());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 62385d9..a7eb0af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -67,7 +67,7 @@
 
   @Override
   protected void formatChange() throws EmailException {
-    appendText(velocifyFile("NewChange.vm"));
+    appendText(textTemplate("NewChange"));
   }
 
   public List<String> getReviewerNames() {
@@ -80,4 +80,10 @@
     }
     return names;
   }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData.put("reviewerNames", getReviewerNames());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
index de338ec..85dd800 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
@@ -25,6 +25,9 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Common class for notifications that are related to a project and branch
  */
@@ -103,4 +106,20 @@
     velocityContext.put("projectName", branch.getParentKey().get());
     velocityContext.put("branch", branch);
   }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+
+    String projectName = branch.getParentKey().get();
+    soyContext.put("projectName", projectName);
+    // shortProjectName is the project name with the path abbreviated.
+    soyContext.put("shortProjectName", projectName.replaceAll("/.*/", "..."));
+
+    soyContextEmailData.put("sshHost", getSshHost());
+
+    Map<String, String> branchData = new HashMap<>();
+    branchData.put("shortName", branch.getShortName());
+    soyContext.put("branch", branchData);
+  }
 }
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 6200688..78fe631 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
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 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.
@@ -18,6 +18,7 @@
 import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
@@ -28,6 +29,7 @@
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
+import com.google.template.soy.data.SanitizedContent;
 
 import org.apache.commons.lang.StringUtils;
 import org.apache.velocity.Template;
@@ -43,8 +45,12 @@
 import java.io.StringWriter;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collection;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -63,9 +69,11 @@
   private final Map<String, EmailHeader> headers;
   private final Set<Address> smtpRcptTo = new HashSet<>();
   private Address smtpFromAddress;
-  private StringBuilder body;
+  private StringBuilder textBody;
+  private StringBuilder htmlBody;
   protected VelocityContext velocityContext;
-
+  protected Map<String, Object> soyContext;
+  protected Map<String, Object> soyContextEmailData;
   protected final EmailArguments args;
   protected Account.Id fromId;
   protected NotifyHandling notify = NotifyHandling.ALL;
@@ -102,7 +110,10 @@
 
     init();
     format();
-    appendText(velocifyFile("Footer.vm"));
+    appendText(textTemplate("Footer"));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("FooterHtml"));
+    }
     if (shouldSendMessage()) {
       if (fromId != null) {
         final Account fromUser = args.accountCache.get(fromId).getAccount();
@@ -136,12 +147,29 @@
         }
       }
 
+      String textPart = textBody.toString();
       OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args();
       va.messageClass = messageClass;
       va.smtpFromAddress = smtpFromAddress;
       va.smtpRcptTo = smtpRcptTo;
       va.headers = headers;
-      va.body = body.toString();
+
+      if (useHtml()) {
+        String htmlPart = htmlBody.toString();
+        String boundary = generateMultipartBoundary(textPart, htmlPart);
+
+        va.body = buildMultipartBody(boundary, textPart, htmlPart);
+        va.textBody = textPart;
+        va.htmlBody = htmlPart;
+        va.headers.put("Content-Type", new EmailHeader.String(
+            "multipart/alternative; "
+            + "boundary=\"" + boundary + "\"; "
+            + "charset=UTF-8"));
+      } else {
+        va.body = textPart;
+        va.textBody = textPart;
+      }
+
       for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) {
         try {
           validator.validateOutgoingEmail(va);
@@ -154,6 +182,49 @@
     }
   }
 
+  protected String buildMultipartBody(String boundary, String textPart,
+      String htmlPart) {
+    return
+        // Output the text part:
+        "--" + boundary + "\r\n"
+        + "Content-Type: text/plain; charset=UTF-8\r\n"
+        + "Content-Transfer-Encoding: 8bit\r\n"
+        + "\r\n"
+        + textPart + "\r\n"
+
+        // Output the HTML part:
+        + "--" + boundary + "\r\n"
+        + "Content-Type: text/html; charset=UTF-8\r\n"
+        + "Content-Transfer-Encoding: 8bit\r\n"
+        + "\r\n"
+        + htmlPart + "\r\n"
+
+        // Output the closing boundary.
+        + "--" + boundary + "--\r\n";
+  }
+
+  protected String generateMultipartBoundary(String textBody, String htmlBody)
+      throws EmailException {
+    byte[] bytes = new byte[8];
+    ThreadLocalRandom rng = ThreadLocalRandom.current();
+
+    // The probability of the boundary being valid is approximately
+    // (2^64 - len(message)) / 2^64.
+    //
+    // The message is much shorter than 2^64 bytes, so if two tries don't
+    // suffice, something is seriously wrong.
+    for (int i = 0; i < 2; i++) {
+      rng.nextBytes(bytes);
+      String boundary = BaseEncoding.base64().encode(bytes);
+      String encBoundary = "--" + boundary;
+      if (textBody.contains(encBoundary) || htmlBody.contains(encBoundary)) {
+        continue;
+      }
+      return boundary;
+    }
+    throw new EmailException("Gave up generating unique MIME boundary");
+  }
+
   /** Format the message body by calling {@link #appendText(String)}. */
   protected abstract void format() throws EmailException;
 
@@ -164,6 +235,7 @@
    */
   protected void init() throws EmailException {
     setupVelocityContext();
+    setupSoyContext();
 
     smtpFromAddress = args.fromAddressGenerator.from(fromId);
     setHeader("Date", new Date());
@@ -185,7 +257,8 @@
     }
 
     setHeader("X-Gerrit-MessageType", messageClass);
-    body = new StringBuilder();
+    textBody = new StringBuilder();
+    htmlBody = new StringBuilder();
 
     if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) {
       appendText(getFromLine());
@@ -260,7 +333,14 @@
   /** Append text to the outgoing email body. */
   protected void appendText(final String text) {
     if (text != null) {
-      body.append(text);
+      textBody.append(text);
+    }
+  }
+
+  /** Append html to the outgoing email body. */
+  protected void appendHtml(String html) {
+    if (html != null) {
+      htmlBody.append(html);
     }
   }
 
@@ -334,7 +414,7 @@
   }
 
   protected boolean shouldSendMessage() {
-    if (body.length() == 0) {
+    if (textBody.length() == 0) {
       // If we have no message body, don't send.
       return false;
     }
@@ -428,6 +508,18 @@
     velocityContext.put("StringUtils", StringUtils.class);
   }
 
+  protected void setupSoyContext() {
+    soyContext = new HashMap<>();
+
+    soyContext.put("messageClass", messageClass);
+
+    soyContextEmailData = new HashMap<>();
+    soyContextEmailData.put("settingsUrl", getSettingsUrl());
+    soyContextEmailData.put("gerritHost", getGerritHost());
+    soyContextEmailData.put("gerritUrl", getGerritUrl());
+    soyContext.put("email", soyContextEmailData);
+  }
+
   protected String velocify(String template) throws EmailException {
     try {
       RuntimeInstance runtime = args.velocityRuntime;
@@ -463,6 +555,37 @@
     }
   }
 
+  private String soyTemplate(String name, SanitizedContent.ContentKind kind) {
+    return args.soyTofu
+        .newRenderer("com.google.gerrit.server.mail.template." + name)
+        .setContentKind(kind)
+        .setData(soyContext)
+        .render();
+  }
+
+  protected String soyTextTemplate(String name) {
+    return soyTemplate(name, SanitizedContent.ContentKind.TEXT);
+  }
+
+  protected String soyHtmlTemplate(String name) {
+    return soyTemplate(name, SanitizedContent.ContentKind.HTML);
+  }
+
+  /**
+   * Evaluate the named template according to the following priority:
+   * 1) Velocity file override, OR...
+   * 2) Soy file override, OR...
+   * 3) Soy resource.
+   */
+  protected String textTemplate(String name) throws EmailException {
+    String velocityName = name + ".vm";
+    Path filePath = args.site.mail_dir.resolve(velocityName);
+    if (Files.isRegularFile(filePath)) {
+      return velocifyFile(velocityName);
+    }
+    return soyTextTemplate(name);
+  }
+
   public String joinStrings(Iterable<Object> in, String joiner) {
     return joinStrings(in.iterator(), joiner);
   }
@@ -504,4 +627,9 @@
   private static String safeToString(Object obj) {
     return obj != null ? obj.toString() : "";
   }
+
+  /** Override this method to enable HTML in a subclass. */
+  protected boolean useHtml() {
+    return false;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
index cfdeb8f..405d9f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -51,7 +51,7 @@
 
   @Override
   protected void format() throws EmailException {
-    appendText(velocifyFile("RegisterNewEmail.vm"));
+    appendText(textTemplate("RegisterNewEmail"));
   }
 
   public String getUserNameEmail() {
@@ -69,4 +69,12 @@
   public boolean isAllowed() {
     return args.emailSender.canEmail(addr);
   }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData
+        .put("emailRegistrationToken", getEmailRegistrationToken());
+    soyContextEmailData.put("userNameEmail", getUserNameEmail());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
index df9f20e..d86aed3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
@@ -72,17 +72,26 @@
 
   @Override
   protected void formatChange() throws EmailException {
-    appendText(velocifyFile("ReplacePatchSet.vm"));
+    appendText(textTemplate("ReplacePatchSet"));
   }
 
   public List<String> getReviewerNames() {
-    if (reviewers.isEmpty()) {
-      return null;
-    }
     List<String> names = new ArrayList<>();
     for (Account.Id id : reviewers) {
+      if (id.equals(fromId)) {
+        continue;
+      }
       names.add(getNameFor(id));
     }
+    if (names.isEmpty()) {
+      return null;
+    }
     return names;
   }
+
+  @Override
+  protected void setupSoyContext() {
+    super.setupSoyContext();
+    soyContextEmailData.put("reviewerNames", getReviewerNames());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index d946eb2..5b6d9bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -49,6 +49,6 @@
 
   @Override
   protected void formatChange() throws EmailException {
-    appendText(velocifyFile("Restored.vm"));
+    appendText(textTemplate("Restored"));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index 2c9c37e..7d1690c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -47,6 +47,6 @@
 
   @Override
   protected void formatChange() throws EmailException {
-    appendText(velocifyFile("Reverted.vm"));
+    appendText(textTemplate("Reverted"));
   }
 }
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 8272aaf..a8e39ee 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
@@ -29,12 +29,12 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC;
 import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Enums;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.Optional;
 import com.google.common.base.Splitter;
-import com.google.common.base.Supplier;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.ImmutableSet;
@@ -46,6 +46,7 @@
 import com.google.common.collect.Table;
 import com.google.common.collect.Tables;
 import com.google.common.primitives.Ints;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.metrics.Timer1;
@@ -85,7 +86,6 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.NavigableSet;
 import java.util.Objects;
 import java.util.Set;
@@ -97,6 +97,20 @@
   private static final RevId PARTIAL_PATCH_SET =
       new RevId("INVALID PARTIAL PATCH SET");
 
+  @AutoValue
+  abstract static class ApprovalKey {
+    abstract PatchSet.Id psId();
+    abstract Account.Id accountId();
+    abstract String label();
+    @Nullable abstract String tag();
+
+    private static ApprovalKey create(PatchSet.Id psId, Account.Id accountId,
+        String label, @Nullable String tag) {
+      return new AutoValue_ChangeNotesParser_ApprovalKey(
+          psId, accountId, label, tag);
+    }
+  }
+
   // Private final members initialized in the constructor.
   private final ChangeNoteUtil noteUtil;
   private final NoteDbMetrics metrics;
@@ -114,8 +128,7 @@
   private final TreeMap<PatchSet.Id, PatchSet> patchSets;
   private final Set<PatchSet.Id> deletedPatchSets;
   private final Map<PatchSet.Id, PatchSetState> patchSetStates;
-  private final Map<PatchSet.Id,
-      Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>>> approvals;
+  private final Map<ApprovalKey, PatchSetApproval> approvals;
   private final List<ChangeMessage> allChangeMessages;
   private final Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet;
 
@@ -142,7 +155,7 @@
     this.walk = walk;
     this.noteUtil = noteUtil;
     this.metrics = metrics;
-    approvals = new HashMap<>();
+    approvals = new LinkedHashMap<>();
     reviewers = HashBasedTable.create();
     allPastReviewers = new ArrayList<>();
     reviewerUpdates = new ArrayList<>();
@@ -210,14 +223,15 @@
   }
 
   private Multimap<PatchSet.Id, PatchSetApproval> buildApprovals() {
-    Multimap<PatchSet.Id, PatchSetApproval> result =
-        ArrayListMultimap.create(approvals.keySet().size(), 3);
-    for (Table<?, ?, Optional<PatchSetApproval>> curr : approvals.values()) {
-      for (Optional<PatchSetApproval> psa : curr.values()) {
-        if (psa.isPresent()) {
-          result.put(psa.get().getPatchSetId(), psa.get());
-        }
+    Multimap<PatchSet.Id, PatchSetApproval> result = ArrayListMultimap.create();
+    for (PatchSetApproval a : approvals.values()) {
+      if (patchSetStates.get(a.getPatchSetId()) == PatchSetState.DELETED) {
+        continue; // Patch set was explicitly deleted.
+      } else if (allPastReviewers.contains(a.getAccountId())
+          && !reviewers.containsRow(a.getAccountId())) {
+        continue; // Reviewer was explicitly removed.
       }
+      result.put(a.getPatchSetId(), a);
     }
     for (Collection<PatchSetApproval> v : result.asMap().values()) {
       Collections.sort((List<PatchSetApproval>) v, ChangeNotes.PSA_BY_TIME);
@@ -606,7 +620,7 @@
           "patch set %s requires an identified user as uploader", psId.get());
     }
     if (line.startsWith("-")) {
-      parseRemoveApproval(psId, accountId, line);
+      parseRemoveApproval(psId, accountId, ts, line);
     } else {
       parseAddApproval(psId, accountId, ts, line);
     }
@@ -637,39 +651,37 @@
       throw pe;
     }
 
-    Entry<String, String> label = Maps.immutableEntry(l.label(), tag);
-    Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr =
-        getApprovalsTableIfNoVotePresent(psId, accountId, label);
-    if (curr != null) {
-      PatchSetApproval psa = new PatchSetApproval(
-          new PatchSetApproval.Key(
-              psId,
-              accountId,
-              new LabelId(l.label())),
-          l.value(),
-          ts);
-      psa.setTag(tag);
-      curr.put(accountId, label, Optional.of(psa));
+    PatchSetApproval psa = new PatchSetApproval(
+        new PatchSetApproval.Key(
+            psId,
+            accountId,
+            new LabelId(l.label())),
+        l.value(),
+        ts);
+    psa.setTag(tag);
+    ApprovalKey k = ApprovalKey.create(psId, accountId, l.label(), tag);
+    if (!approvals.containsKey(k)) {
+      approvals.put(k, psa);
     }
   }
 
   private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId,
-      String line) throws ConfigInvalidException {
+      Timestamp ts, String line) throws ConfigInvalidException {
     Account.Id accountId;
-    Entry<String, String> label;
+    String label;
     int s = line.indexOf(' ');
     if (s > 0) {
-      label = Maps.immutableEntry(line.substring(1, s), tag);
+      label = line.substring(1, s);
       PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
       checkFooter(ident != null, FOOTER_LABEL, line);
       accountId = noteUtil.parseIdent(ident, id);
     } else {
-      label = Maps.immutableEntry(line.substring(1), tag);
+      label = line.substring(1);
       accountId = committerId;
     }
 
     try {
-      LabelType.checkNameInternal(label.getKey());
+      LabelType.checkNameInternal(label);
     } catch (IllegalArgumentException e) {
       ConfigInvalidException pe =
           parseException("invalid %s: %s", FOOTER_LABEL, line);
@@ -677,38 +689,25 @@
       throw pe;
     }
 
-    Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr =
-        getApprovalsTableIfNoVotePresent(psId, accountId, label);
-    if (curr != null) {
-      curr.put(accountId, label, Optional.<PatchSetApproval> absent());
+    // Store an actual 0-vote approval in the map for a removed approval, for
+    // several reasons:
+    //  - This is closer to the ReviewDb representation, which leads to less
+    //    confusion and special-casing of NoteDb.
+    //  - More importantly, ApprovalCopier needs an actual approval in order to
+    //    block copying an earlier approval over a later delete.
+    PatchSetApproval remove = new PatchSetApproval(
+        new PatchSetApproval.Key(
+            psId,
+            accountId,
+            new LabelId(label)),
+        (short) 0,
+        ts);
+    ApprovalKey k = ApprovalKey.create(psId, accountId, label, tag);
+    if (!approvals.containsKey(k)) {
+      approvals.put(k, remove);
     }
   }
 
-  private Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>>
-      getApprovalsTableIfNoVotePresent(PatchSet.Id psId, Account.Id accountId,
-        Entry<String, String> label) {
-
-    Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr =
-        approvals.get(psId);
-    if (curr != null) {
-      if (curr.contains(accountId, label)) {
-        return null;
-      }
-    } else {
-      curr = Tables.newCustomTable(
-          Maps.<Account.Id, Map<Entry<String, String>, Optional<PatchSetApproval>>>
-              newHashMapWithExpectedSize(2),
-          new Supplier<Map<Entry<String, String>, Optional<PatchSetApproval>>>() {
-            @Override
-            public Map<Entry<String, String>, Optional<PatchSetApproval>> get() {
-              return new LinkedHashMap<>();
-            }
-          });
-      approvals.put(psId, curr);
-    }
-    return curr;
-  }
-
   private void parseSubmitRecords(List<String> lines)
       throws ConfigInvalidException {
     SubmitRecord rec = null;
@@ -787,9 +786,6 @@
       Table.Cell<Account.Id, ReviewerStateInternal, Timestamp> e = rit.next();
       if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
         rit.remove();
-        for (Table<Account.Id, ?, ?> curr : approvals.values()) {
-          curr.rowKeySet().remove(e.getRowKey());
-        }
       }
     }
   }
@@ -832,13 +828,14 @@
     // Post-process other collections to remove items corresponding to deleted
     // patch sets. This is safer than trying to prevent insertion, as it will
     // also filter out items racily added after the patch set was deleted.
+    //
+    // Approvals are filtered in buildApprovals().
     NavigableSet<PatchSet.Id> all = patchSets.navigableKeySet();
     if (!all.isEmpty()) {
       currentPatchSetId = all.last();
     } else {
       currentPatchSetId = null;
     }
-    approvals.keySet().retainAll(all);
     changeMessagesByPatchSet.keys().retainAll(all);
 
     for (Iterator<ChangeMessage> it = allChangeMessages.iterator();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index e570b3a..f8c8b49 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.patch;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.reviewdb.client.Patch;
 
@@ -151,7 +153,7 @@
       return new Text(repo.open(tw.getObjectId(0), Constants.OBJ_BLOB));
     } else if (tw.getFileMode(0).getObjectType() == Constants.OBJ_COMMIT) {
       String str = "Subproject commit " + ObjectId.toString(tw.getObjectId(0));
-      return new Text(str.getBytes());
+      return new Text(str.getBytes(UTF_8));
     } else {
       return Text.EMPTY;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
index 2ccc9f1..fab66cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
@@ -21,6 +21,10 @@
     super(message);
   }
 
+  public PatchListNotAvailableException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
   public PatchListNotAvailableException(Throwable cause) {
     super(cause);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index a7d2523..754d995 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.patch;
 
 import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.server.util.GitUtil.getParent;
 
 import com.google.common.base.Optional;
 import com.google.gerrit.common.Nullable;
@@ -214,8 +213,6 @@
       bId = toObjectId(psEntityB);
       if (parentNum < 0) {
         aId = psEntityA != null ? toObjectId(psEntityA) : null;
-      } else {
-        aId = getParent(git, bId, parentNum);
       }
 
       try {
@@ -247,7 +244,10 @@
   }
 
   private PatchListKey keyFor(final Whitespace whitespace) {
-    return new PatchListKey(aId, bId, whitespace);
+    if (parentNum < 0) {
+      return new PatchListKey(aId, bId, whitespace);
+    }
+    return PatchListKey.againstParentNum(parentNum + 1, bId, whitespace);
   }
 
   private PatchList listFor(final PatchListKey key)
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 9086b6a..5bc7f40 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
@@ -269,7 +269,7 @@
 
   /** Can this user rebase this change? */
   public boolean canRebase(ReviewDb db) throws OrmException {
-    return (isOwner() || getRefControl().canSubmit()
+    return (isOwner() || getRefControl().canSubmit(isOwner())
         || getRefControl().canRebase()) && !isPatchSetLocked(db);
   }
 
@@ -424,7 +424,7 @@
   }
 
   public boolean canSubmit() {
-    return getRefControl().canSubmit();
+    return getRefControl().canSubmit(isOwner());
   }
 
   public boolean canSubmitAs() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
index 446fa72..9e090c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
@@ -116,7 +116,7 @@
       if (isSigned) {
         throw new MethodNotAllowedException(
             "Cannot create signed tag \"" + ref + "\"");
-      } else if (isAnnotated && !refControl.canPerform(Permission.PUSH_TAG)) {
+      } else if (isAnnotated && !refControl.canPerform(Permission.CREATE_TAG)) {
         throw new AuthException("Cannot create annotated tag \"" + ref + "\"");
       } else if (!refControl.canPerform(Permission.CREATE)) {
         throw new AuthException("Cannot create tag \"" + ref + "\"");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
index 47942be..82ea155 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
@@ -14,16 +14,39 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestResource;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.TypeLiteral;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
 
 public class FileResource implements RestResource {
   public static final TypeLiteral<RestView<FileResource>> FILE_KIND =
       new TypeLiteral<RestView<FileResource>>() {};
 
+  public static FileResource create(GitRepositoryManager repoManager,
+      ProjectControl project, ObjectId rev, String path)
+          throws ResourceNotFoundException, IOException {
+    try (Repository repo =
+            repoManager.openRepository(project.getProject().getNameKey());
+        RevWalk rw = new RevWalk(repo)) {
+      RevTree tree = rw.parseTree(rev);
+      if (TreeWalk.forPath(repo, path, tree) != null) {
+        return new FileResource(project, rev, path);
+      }
+    }
+    throw new ResourceNotFoundException(IdString.fromDecoded(path));
+  }
+
   private final ProjectControl project;
   private final ObjectId rev;
   private final String path;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
index d0460d5..dcb8747 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
@@ -19,19 +19,25 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectId;
 
+import java.io.IOException;
+
 @Singleton
 public class FilesCollection implements
     ChildCollection<BranchResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
+  private final GitRepositoryManager repoManager;
 
   @Inject
-  FilesCollection(DynamicMap<RestView<FileResource>> views) {
+  FilesCollection(DynamicMap<RestView<FileResource>> views,
+      GitRepositoryManager repoManager) {
     this.views = views;
+    this.repoManager = repoManager;
   }
 
   @Override
@@ -40,11 +46,10 @@
   }
 
   @Override
-  public FileResource parse(BranchResource parent, IdString id) {
-    return new FileResource(
-        parent.getControl(),
-        ObjectId.fromString(parent.getRevision()),
-        id.get());
+  public FileResource parse(BranchResource parent, IdString id)
+      throws ResourceNotFoundException, IOException {
+    return FileResource.create(repoManager, parent.getControl(),
+        ObjectId.fromString(parent.getRevision()), id.get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
index 8e0aab8..64a5fb2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
@@ -19,17 +19,24 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import java.io.IOException;
+
 @Singleton
 public class FilesInCommitCollection implements
     ChildCollection<CommitResource, FileResource> {
   private final DynamicMap<RestView<FileResource>> views;
+  private final GitRepositoryManager repoManager;
 
   @Inject
-  FilesInCommitCollection(DynamicMap<RestView<FileResource>> views) {
+  FilesInCommitCollection(DynamicMap<RestView<FileResource>> views,
+      GitRepositoryManager repoManager) {
     this.views = views;
+    this.repoManager = repoManager;
   }
 
   @Override
@@ -39,8 +46,13 @@
 
   @Override
   public FileResource parse(CommitResource parent, IdString id)
-      throws ResourceNotFoundException {
-    return new FileResource(parent.getProject(), parent.getCommit(), id.get());
+      throws ResourceNotFoundException, IOException {
+    if (Patch.COMMIT_MSG.equals(id.get())) {
+      return new FileResource(parent.getProject(), parent.getCommit(),
+          id.get());
+    }
+    return FileResource.create(repoManager, parent.getProject(),
+        parent.getCommit(), id.get());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
index a862ac2..ce1bab4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -18,8 +18,10 @@
 import static com.google.gerrit.server.project.RefPattern.isRE;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
@@ -116,6 +118,8 @@
       HashMap<String, List<PermissionRule>> permissions = new HashMap<>();
       HashMap<String, List<PermissionRule>> overridden = new HashMap<>();
       Map<PermissionRule, ProjectRef> ruleProps = Maps.newIdentityHashMap();
+      Multimap<Project.NameKey, String> exclusivePermissionsByProject =
+          ArrayListMultimap.create();
       for (AccessSection section : sections) {
         Project.NameKey project = sectionToProject.get(section);
         for (Permission permission : section.getPermissions()) {
@@ -126,7 +130,8 @@
             SeenRule s = SeenRule.create(section, permission, rule);
             boolean addRule;
             if (rule.isBlock()) {
-              addRule = true;
+              addRule = !exclusivePermissionsByProject.containsEntry(project,
+                  permission.getName());
             } else {
               addRule = seen.add(s) && !rule.isDeny() && !exclusivePermissionExists;
             }
@@ -150,6 +155,7 @@
           }
 
           if (permission.getExclusiveGroup()) {
+            exclusivePermissionsByProject.put(project, permission.getName());
             exclusiveGroupPermissions.add(permission.getName());
           }
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 22e5d69..d9cc59c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -307,8 +307,9 @@
 
   /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
   public boolean isOwner() {
-    return isDeclaredOwner()
-      || user.getCapabilities().canAdministrateServer();
+    return (isDeclaredOwner()
+        && !controlForRef("refs/*").isBlocked(Permission.OWNER))
+        || user.getCapabilities().canAdministrateServer();
   }
 
   private boolean isDeclaredOwner() {
@@ -327,8 +328,8 @@
 
   /** @return true if the user can upload to at least one reference */
   public Capable canPushToAtLeastOneRef() {
-    if (! canPerformOnAnyRef(Permission.PUSH) &&
-        ! canPerformOnAnyRef(Permission.PUSH_TAG)) {
+    if (!canPerformOnAnyRef(Permission.PUSH) &&
+        !canPerformOnAnyRef(Permission.CREATE_TAG)) {
       String pName = state.getProject().getName();
       return new Capable("Upload denied for project '" + pName + "'");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
index e06fb86..52bbdf3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.BranchInfo;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -23,7 +24,7 @@
 public class PutBranch implements RestModifyView<BranchResource, BranchInput> {
 
   @Override
-  public Object apply(BranchResource rsrc, BranchInput input)
+  public BranchInfo apply(BranchResource rsrc, BranchInput input)
       throws ResourceConflictException {
     throw new ResourceConflictException("Branch \"" + rsrc.getBranchInfo().ref
         + "\" already exists");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutTag.java
index a87882e..1be4b0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutTag.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutTag.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.extensions.api.projects.TagInfo;
 import com.google.gerrit.extensions.api.projects.TagInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -21,7 +22,7 @@
 public class PutTag implements RestModifyView<TagResource, TagInput> {
 
   @Override
-  public Object apply(TagResource resource, TagInput input)
+  public TagInfo apply(TagResource resource, TagInput input)
       throws ResourceConflictException {
     throw new ResourceConflictException("Tag \"" + resource.getTagInfo().ref
         + "\" already exists");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index ad41522..0365855 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -171,7 +171,7 @@
   }
 
   /** @return true if this user can submit patch sets to this ref */
-  public boolean canSubmit() {
+  public boolean canSubmit(boolean isChangeOwner) {
     if (RefNames.REFS_CONFIG.equals(refName)) {
       // Always allow project owners to submit configuration changes.
       // Submitting configuration changes modifies the access control
@@ -180,7 +180,7 @@
       // granting of powers beyond submitting to the configuration.
       return projectControl.isOwner();
     }
-    return canPerform(Permission.SUBMIT)
+    return canPerform(Permission.SUBMIT, isChangeOwner)
         && canWrite();
   }
 
@@ -213,7 +213,27 @@
 
   /** @return true if the user can rewind (force push) the reference. */
   public boolean canForceUpdate() {
-    return (canPushWithForce() || canDelete()) && canWrite();
+    if (!canWrite()) {
+      return false;
+    }
+
+    if (canPushWithForce()) {
+      return true;
+    }
+
+    switch (getUser().getAccessPath()) {
+      case GIT:
+        return false;
+
+      case JSON_RPC:
+      case REST_API:
+      case SSH_COMMAND:
+      case UNKNOWN:
+      case WEB_BROWSER:
+      default:
+        return getUser().getCapabilities().canAdministrateServer()
+            || (isOwner() && !isForceBlocked(Permission.PUSH));
+    }
   }
 
   public boolean canWrite() {
@@ -251,43 +271,13 @@
     if (!canWrite()) {
       return false;
     }
-    boolean owner;
-    boolean admin;
-    switch (getUser().getAccessPath()) {
-      case REST_API:
-      case JSON_RPC:
-      case UNKNOWN:
-        owner = isOwner();
-        admin = getUser().getCapabilities().canAdministrateServer();
-        break;
-
-      case GIT:
-      case SSH_COMMAND:
-      case WEB_BROWSER:
-      default:
-        owner = false;
-        admin = false;
-    }
 
     if (object instanceof RevCommit) {
-      if (admin || (owner && !isBlocked(Permission.CREATE))) {
-        // Admin or project owner; bypass visibility check.
-        return true;
-      } else if (!canPerform(Permission.CREATE)) {
+      if (!canPerform(Permission.CREATE)) {
         // No create permissions.
         return false;
-      } else if (canUpdate()) {
-        // If the user has push permissions, they can create the ref regardless
-        // of whether they are pushing any new objects along with the create.
-        return true;
-      } else if (isMergedIntoBranchOrTag(db, repo, (RevCommit) object)) {
-        // If the user has no push permissions, check whether the object is
-        // merged into a branch or tag readable by this user. If so, they are
-        // not effectively "pushing" more objects, so they can create the ref
-        // even if they don't have push permission.
-        return true;
       }
-      return false;
+      return canCreateCommit(db, repo, (RevCommit) object);
     } else if (object instanceof RevTag) {
       final RevTag tag = (RevTag) object;
       try (RevWalk rw = new RevWalk(repo)) {
@@ -307,7 +297,18 @@
         } else {
           valid = false;
         }
-        if (!valid && !owner && !canForgeCommitter()) {
+        if (!valid && !canForgeCommitter()) {
+          return false;
+        }
+      }
+
+      RevObject tagObject = tag.getObject();
+      if (tagObject instanceof RevCommit) {
+        if (!canCreateCommit(db, repo, (RevCommit) tagObject)) {
+          return false;
+        }
+      } else {
+        if (!canCreate(db, repo, tagObject)) {
           return false;
         }
       }
@@ -316,14 +317,30 @@
       // than if it doesn't have a PGP signature.
       //
       if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
-        return owner || canPerform(Permission.PUSH_SIGNED_TAG);
+        return canPerform(Permission.CREATE_SIGNED_TAG);
       }
-      return owner || canPerform(Permission.PUSH_TAG);
+      return canPerform(Permission.CREATE_TAG);
     } else {
       return false;
     }
   }
 
+  private boolean canCreateCommit(ReviewDb db, Repository repo,
+      RevCommit commit) {
+    if (canUpdate()) {
+      // If the user has push permissions, they can create the ref regardless
+      // of whether they are pushing any new objects along with the create.
+      return true;
+    } else if (isMergedIntoBranchOrTag(db, repo, commit)) {
+      // If the user has no push permissions, check whether the object is
+      // merged into a branch or tag readable by this user. If so, they are
+      // not effectively "pushing" more objects, so they can create the ref
+      // even if they don't have push permission.
+      return true;
+    }
+    return false;
+  }
+
   private boolean isMergedIntoBranchOrTag(ReviewDb db, Repository repo,
       RevCommit commit) {
     try (RevWalk rw = new RevWalk(repo)) {
@@ -359,7 +376,7 @@
 
     switch (getUser().getAccessPath()) {
       case GIT:
-        return canPushWithForce();
+        return canPushWithForce() || canPerform(Permission.DELETE);
 
       case JSON_RPC:
       case REST_API:
@@ -369,7 +386,8 @@
       default:
         return getUser().getCapabilities().canAdministrateServer()
             || (isOwner() && !isForceBlocked(Permission.PUSH))
-            || canPushWithForce();
+            || canPushWithForce()
+            || canPerform(Permission.DELETE);
     }
   }
 
@@ -531,16 +549,21 @@
 
   /** True if the user has this permission. Works only for non labels. */
   boolean canPerform(String permissionName) {
-    return doCanPerform(permissionName, false);
+    return canPerform(permissionName, false);
+  }
+
+  boolean canPerform(String permissionName, boolean isChangeOwner) {
+    return doCanPerform(permissionName, isChangeOwner, false);
   }
 
   /** True if the user is blocked from using this permission. */
   public boolean isBlocked(String permissionName) {
-    return !doCanPerform(permissionName, true);
+    return !doCanPerform(permissionName, false, true);
   }
 
-  private boolean doCanPerform(String permissionName, boolean blockOnly) {
-    List<PermissionRule> access = access(permissionName);
+  private boolean doCanPerform(String permissionName, boolean isChangeOwner,
+      boolean blockOnly) {
+    List<PermissionRule> access = access(permissionName, isChangeOwner);
     List<PermissionRule> overridden = relevant.getOverridden(permissionName);
     Set<ProjectRef> allows = new HashSet<>();
     Set<ProjectRef> blocks = new HashSet<>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
index cda548a..594763e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
@@ -20,7 +20,9 @@
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 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.server.project.DashboardsCollection.DashboardInfo;
 import com.google.gerrit.server.project.SetDashboard.Input;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -44,7 +46,7 @@
   }
 
   @Override
-  public Object apply(DashboardResource resource, Input input)
+  public Response<DashboardInfo> apply(DashboardResource resource, Input input)
       throws AuthException, BadRequestException, ResourceConflictException,
       MethodNotAllowedException, ResourceNotFoundException, IOException {
     if (resource.isProjectDefault()) {
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 a260d02..3e8b5e42 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
@@ -751,6 +751,7 @@
     if (change == null) {
       throw new OrmException("Unable to load change " + legacyId);
     }
+    setPatchSets(null);
     return change;
   }
 
@@ -784,8 +785,16 @@
       if (c == null) {
         currentApprovals = Collections.emptyList();
       } else {
-        currentApprovals = ImmutableList.copyOf(approvalsUtil.byPatchSet(
-            db, changeControl(), c.currentPatchSetId()));
+        try {
+          currentApprovals = ImmutableList.copyOf(approvalsUtil.byPatchSet(
+              db, changeControl(), c.currentPatchSetId()));
+        } catch (OrmException e) {
+          if (e.getCause() instanceof NoSuchChangeException) {
+            currentApprovals = Collections.emptyList();
+          } else {
+            throw e;
+          }
+        }
       }
     }
     return currentApprovals;
@@ -1002,9 +1011,17 @@
         mergeable = true;
       } else {
         PatchSet ps = currentPatchSet();
-        if (ps == null || !changeControl().isPatchVisible(ps, db)) {
-          return null;
+        try {
+          if (ps == null || !changeControl().isPatchVisible(ps, db)) {
+            return null;
+          }
+        } catch (OrmException e) {
+          if (e.getCause() instanceof NoSuchChangeException) {
+            return null;
+          }
+          throw e;
         }
+
         try (Repository repo = repoManager.openRepository(project())) {
           Ref ref = repo.getRefDatabase().exactRef(c.getDest().get());
           SubmitTypeRecord str = submitTypeRecord();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 7c7417a..1bc878e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -165,8 +165,9 @@
       grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
       grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
 
-      grant(config, tags, Permission.PUSH_TAG, admin, owners);
-      grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
+      grant(config, tags, Permission.CREATE, admin, owners);
+      grant(config, tags, Permission.CREATE_TAG, admin, owners);
+      grant(config, tags, Permission.CREATE_SIGNED_TAG, admin, owners);
 
       grant(config, magic, Permission.PUSH, registered);
       grant(config, magic, Permission.PUSH_MERGE, registered);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
new file mode 100644
index 0000000..53ed5ac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
@@ -0,0 +1,109 @@
+// 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 static com.google.gerrit.server.git.ProjectConfig.ACCESS;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+public class ProjectConfigSchemaUpdate extends VersionedMetaData {
+
+  private final MetaDataUpdate update;
+  private Config config;
+  private boolean updated;
+
+  public static ProjectConfigSchemaUpdate read(MetaDataUpdate update)
+      throws IOException, ConfigInvalidException {
+    ProjectConfigSchemaUpdate r = new ProjectConfigSchemaUpdate(update);
+    r.load(update);
+    return r;
+  }
+
+  private ProjectConfigSchemaUpdate(MetaDataUpdate update) {
+    this.update = update;
+  }
+
+  @Override
+  protected String getRefName() {
+    return RefNames.REFS_CONFIG;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    config = readConfig(ProjectConfig.PROJECT_CONFIG);
+  }
+
+  public void removeForceFromPermission(String name) {
+    for (String subsection : config.getSubsections(ACCESS)) {
+      Set<String> names = config.getNames(ACCESS, subsection);
+      if (names.contains(name)) {
+        List<String> values =
+            Arrays.asList(config.getStringList(ACCESS, subsection, name));
+        values = Lists.transform(values, new Function<String, String>() {
+          @Override
+          public String apply(String ruleString) {
+            PermissionRule rule = PermissionRule.fromString(ruleString, false);
+            if (rule.getForce()) {
+              rule.setForce(false);
+              updated = true;
+            }
+            return rule.asString(false);
+          }
+        });
+        config.setStringList(ACCESS, subsection, name, values);
+      }
+    }
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit)
+      throws IOException, ConfigInvalidException {
+    saveConfig(ProjectConfig.PROJECT_CONFIG, config);
+    return true;
+  }
+
+  public void save(PersonIdent personIdent, String commitMessage)
+      throws OrmException {
+    if (!updated) {
+      return;
+    }
+
+    update.getCommitBuilder().setAuthor(personIdent);
+    update.getCommitBuilder().setCommitter(personIdent);
+    update.setMessage(commitMessage);
+    try {
+      commit(update);
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+  }
+}
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 7217fd0..5d1e579 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -33,7 +33,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_129> C = Schema_129.class;
+  public static final Class<Schema_131> C = Schema_131.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
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 9fdec25..cd42e75 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
@@ -136,6 +136,8 @@
           p.reviewCategoryStrategy =
               toReviewCategoryStrategy(rs.getString(14));
           p.muteCommonPathPrefixes = toBoolean(rs.getString(15));
+          p.defaultBaseForMerges =
+              GeneralPreferencesInfo.defaults().defaultBaseForMerges;
           imports.put(accountId, p);
         }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_130.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_130.java
new file mode 100644
index 0000000..d7fcc3b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_130.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.server.schema;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+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.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+public class Schema_130 extends SchemaVersion {
+  private static final String COMMIT_MSG =
+      "Remove force option from 'Push Annotated Tag' permission\n"
+      + "\n"
+      + "The force option on 'Push Annotated Tag' had no effect and is no longer\n"
+      + "supported.";
+
+  private final GitRepositoryManager repoManager;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_130(Provider<Schema_129> prior,
+      GitRepositoryManager repoManager,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    for (Project.NameKey projectName : repoManager.list()) {
+      try (Repository git = repoManager.openRepository(projectName);
+          MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+              projectName, git)) {
+        ProjectConfigSchemaUpdate cfg = ProjectConfigSchemaUpdate.read(md);
+        cfg.removeForceFromPermission("pushTag");
+        cfg.save(serverUser, COMMIT_MSG);
+      } catch (ConfigInvalidException | IOException ex) {
+        throw new OrmException(ex);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_131.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_131.java
new file mode 100644
index 0000000..1be4c3e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_131.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.schema;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+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.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+public class Schema_131 extends SchemaVersion {
+  private static final String COMMIT_MSG =
+      "Rename 'Push Annotated/Signed Tag' permission to 'Create Annotated/Signed Tag'";
+
+  private final GitRepositoryManager repoManager;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_131(Provider<Schema_130> prior,
+      GitRepositoryManager repoManager,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    for (Project.NameKey projectName : repoManager.list()) {
+      try (Repository git = repoManager.openRepository(projectName);
+          MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+              projectName, git)) {
+        ProjectConfig config = ProjectConfig.read(md);
+        if (config.hasLegacyPermissions()) {
+          md.getCommitBuilder().setAuthor(serverUser);
+          md.getCommitBuilder().setCommitter(serverUser);
+          md.setMessage(COMMIT_MSG);
+          config.commit(md);
+        }
+      } catch (ConfigInvalidException | IOException ex) {
+        throw new OrmException(ex);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java
deleted file mode 100644
index 2d1e1fa..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java
+++ /dev/null
@@ -1,51 +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.
-
-package com.google.gerrit.server.util;
-
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-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;
-
-public class GitUtil {
-
-  /**
-   * @param git
-   * @param commitId
-   * @param parentNum
-   * @return the {@code paretNo} parent of given commit or {@code null}
-   *             when {@code parentNo} exceed number of {@code commitId} parents.
-   * @throws IncorrectObjectTypeException
-   *             the supplied id is not a commit or an annotated tag.
-   * @throws IOException
-   *             a pack file or loose object could not be read.
-   */
-  public static RevCommit getParent(Repository git,
-      ObjectId commitId, int parentNum) throws IOException {
-    try (RevWalk walk = new RevWalk(git)) {
-      RevCommit commit = walk.parseCommit(commitId);
-      if (commit.getParentCount() > parentNum) {
-        return commit.getParent(parentNum);
-      }
-    }
-    return null;
-  }
-
-  private GitUtil() {
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
index fab0b34..030383a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
@@ -58,6 +58,16 @@
         Short.parseShort(text.substring(e + 1), text.length()));
   }
 
+  public static StringBuilder appendTo(StringBuilder sb, String label,
+      short value) {
+    if (value == (short) 0) {
+      return sb.append('-').append(label);
+    } else if (value < 0) {
+      return sb.append(label).append(value);
+    }
+    return sb.append(label).append('+').append(value);
+  }
+
   public static LabelVote create(String label, short value) {
     return new AutoValue_LabelVote(LabelType.checkNameInternal(label), value);
   }
@@ -70,13 +80,9 @@
   public abstract short value();
 
   public String format() {
-    if (value() == (short) 0) {
-      return '-' + label();
-    } else if (value() < 0) {
-      return label() + value();
-    } else {
-      return label() + '+' + value();
-    }
+    // Max short string length is "-32768".length() == 6.
+    return appendTo(new StringBuilder(label().length() + 6), label(), value())
+        .toString();
   }
 
   public String formatWithEquals() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
index b2899c1..bfec979 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.validators;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.ExtensionPoint;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.EmailHeader;
@@ -32,6 +33,8 @@
   class Args {
     // in arguments
     public String messageClass;
+    public String textBody;
+    @Nullable public String htmlBody;
 
     // in/out arguments
     public Address smtpFromAddress;
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy
new file mode 100644
index 0000000..50c5fc3
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy
@@ -0,0 +1,39 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * .Abandoned template will determine the contents of the email related to a
+ * change being abandoned.
+ * @param change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .Abandoned autoescape="strict" kind="text"}
+  {$fromName} has abandoned this change.
+  {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
+  {\n}
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {if $coverLetter}
+    {\n}
+    {\n}
+    {$coverLetter}
+    {\n}
+  {/if}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm
deleted file mode 100644
index accd3b8..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm
+++ /dev/null
@@ -1,46 +0,0 @@
-## Copyright (C) 2010 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 Abandoned.vm template will determine the contents of the email related
-## to a change being abandoned.   It is a ChangeEmail: see ChangeSubject.vm and
-## ChangeFooter.vm.
-##
-$fromName has abandoned this change.#**
-*##if($email.changeUrl) ( $email.changeUrl )#end
-
-
-Change subject: $change.subject
-......................................................................
-
-
-#if ($coverLetter)
-$coverLetter
-
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy
new file mode 100644
index 0000000..aa2b27d
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy
@@ -0,0 +1,71 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .AddKey template will determine the contents of the email related to
+ * adding a new SSH or GPG key to an account.
+ * @param email
+ */
+{template .AddKey autoescape="strict" kind="text"}
+  One or more new {$email.keyType} keys have been added to Gerrit Code Review at
+  {sp}{$email.gerritHost}:
+
+  {\n}
+  {\n}
+
+  {if $email.sshKey}
+    {$email.sshKey}
+  {elseif $email.gpgKeys}
+    {$email.gpgKeys}
+  {/if}
+
+  {\n}
+  {\n}
+
+  If this is not expected, please contact your Gerrit Administrators
+  immediately.
+
+  {\n}
+  {\n}
+
+  You can also manage your {$email.keyType} keys by visiting
+  {\n}
+  {if $email.sshKey}
+    {$email.gerritUrl}#/settings/ssh-keys
+  {elseif $email.gpgKeys}
+    {$email.gerritUrl}#/settings/gpg-keys
+  {/if}
+  {\n}
+  {if $email.userNameEmail}
+    (while signed in as {$email.userNameEmail})
+  {else}
+    (while signed in as {$email.email})
+  {/if}
+
+  {\n}
+  {\n}
+
+  If clicking the link above does not work, copy and paste the URL in a new
+  browser window instead.
+
+  {\n}
+  {\n}
+
+  This is a send-only email address.  Replies to this message will not be read
+  or answered.
+{/template}
\ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm
deleted file mode 100644
index c60ce8b..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm
+++ /dev/null
@@ -1,61 +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.
-##
-##
-## 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 AddKey.vm template will determine the contents of the email
-## related to adding a new SSH or GPG key to an account.
-##
-One or more new ${email.keyType} keys have been added to Gerrit Code Review at ${email.gerritHost}:
-
-#if($email.sshKey)
-$email.sshKey
-#elseif($email.gpgKeys)
-$email.gpgKeys
-#end
-
-If this is not expected, please contact your Gerrit Administrators
-immediately.
-
-You can also manage your ${email.keyType} keys by visiting
-#if($email.sshKey)
-$email.gerritUrl#/settings/ssh-keys
-#elseif($email.gpgKeys)
-$email.gerritUrl#/settings/gpg-keys
-#end
-#if($email.userNameEmail)
-(while signed in as $email.userNameEmail)
-#else
-(while signed in as $email.email)
-#end
-
-If clicking the link above does not work, copy and paste the URL in a
-new browser window instead.
-
-This is a send-only email address.  Replies to this message will not
-be read or answered.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
new file mode 100644
index 0000000..017fd6d
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
@@ -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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * @param email
+ */
+{template .AddKeyHtml autoescape="strict" kind="html"}
+  <p>
+    One or more new {$email.keyType} keys have been added to Gerrit Code Review
+    at {$email.gerritHost}:
+  </p>
+
+  {let $keyStyle kind="css"}
+    background: #f0f0f0;
+    border: 1px solid #ccc;
+    color: #555;
+    padding: 12px;
+    width: 400px;
+  {/let}
+
+  {if $email.sshKey}
+    <pre style="{$keyStyle}">{$email.sshKey}</pre>
+  {elseif $email.gpgKeys}
+    <pre style="{$keyStyle}">{$email.gpgKeys}</pre>
+  {/if}
+
+  <p>
+    If this is not expected, please contact your Gerrit Administrators
+    immediately.
+  </p>
+
+  <p>
+    You can also manage your {$email.keyType} keys by following{sp}
+    {if $email.sshKey}
+      <a href="{$email.gerritUrl}#/settings/ssh-keys">this link</a>
+    {elseif $email.gpgKeys}
+      <a href="{$email.gerritUrl}#/settings/gpg-keys">this link</a>
+    {/if}
+    {sp}
+    {if $email.userNameEmail}
+      (while signed in as {$email.userNameEmail})
+    {else}
+      (while signed in as {$email.email})
+    {/if}.
+  </p>
+
+  <p>
+    This is a send-only email address.  Replies to this message will not be read
+    or answered.
+  </p>
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
new file mode 100644
index 0000000..9906dd8d
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
@@ -0,0 +1,52 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .ChangeFooter template will determine the contents of the footer text
+ * that will be appended to ALL emails related to changes.
+ * @param branch
+ * @param change
+ * @param changeId
+ * @param email
+ * @param messageClass
+ * @param patchSet
+ * @param projectName
+ */
+{template .ChangeFooter autoescape="strict" kind="text"}
+  --{sp}
+  {\n}
+
+  {if $email.changeUrl}
+    To view, visit {$email.changeUrl}{\n}
+  {/if}
+
+  {if $email.settingsUrl}
+    To unsubscribe, visit {$email.settingsUrl}{\n}
+  {/if}
+
+  {if $email.changeUrl or $email.settingsUrl}
+    {\n}
+  {/if}
+
+  Gerrit-MessageType: {$messageClass}{\n}
+  Gerrit-Change-Id: {$changeId}{\n}
+  Gerrit-PatchSet: {$patchSet.patchSetId}{\n}
+  Gerrit-Project: {$projectName}{\n}
+  Gerrit-Branch: {$branch.shortName}{\n}
+  Gerrit-Owner: {$change.ownerEmail}{\n}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
deleted file mode 100644
index f1d3e90..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
+++ /dev/null
@@ -1,52 +0,0 @@
-## Copyright (C) 2010 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 ChangeFooter.vm template will determine the contents of the footer
-## text that will be appended to ALL emails related to changes.
-##
-#set ($SPACE = " ")
---$SPACE
-#if ($email.changeUrl)
-To view, visit $email.changeUrl
-#set ($notblank = 1)
-#end
-#if ($email.settingsUrl)
-To unsubscribe, visit $email.settingsUrl
-#set ($notblank = 1)
-#end
-#if ($notblank)
-
-#end
-Gerrit-MessageType: $messageClass
-Gerrit-Change-Id: $changeId
-Gerrit-PatchSet: $patchSet.patchSetId
-Gerrit-Project: $projectName
-Gerrit-Branch: $branch.shortName
-Gerrit-Owner: $email.getNameEmailFor($change.owner)
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy
new file mode 100644
index 0000000..98de6e7
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy
@@ -0,0 +1,28 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .ChangeSubject template will determine the contents of the email subject
+ * line for ALL emails related to changes.
+ * @param branch
+ * @param change
+ * @param shortProjectName
+ */
+{template .ChangeSubject autoescape="strict" kind="text"}
+  Change in {$shortProjectName}[{$branch.shortName}]: {$change.shortSubject}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
deleted file mode 100644
index 4fd9a23..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
+++ /dev/null
@@ -1,42 +0,0 @@
-## Copyright (C) 2010 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 ChangeSubject.vm template will determine the contents of the email
-## subject line for ALL emails related to changes.
-##
-## Optionally $change.originalSubject can be used for the first subject
-## in a change. This allows subject based email clients such as GMail
-## to thread comments together even if subsequent patch sets change the
-## first line of the commit message.
-##
-#macro(ellipsis $length $str)
-#if($str.length() > $length)#set($length = $length - 3)${str.substring(0,$length)}...#else$str#end
-#end
-Change in ${projectName.replaceAll('/.*/', '...')}[$branch.shortName]: #ellipsis(63, $change.subject)
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy
new file mode 100644
index 0000000..781d8a0
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy
@@ -0,0 +1,46 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .Comment template will determine the contents of the email related to a
+ * user submitting comments on changes.
+ * @param change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .Comment autoescape="strict" kind="text"}
+  {if $coverLetter or $email.hasInlineComments}
+    {$fromName} has posted comments on this change.
+    {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
+    {\n}
+    Change subject: {$change.subject}{\n}
+    ......................................................................{\n}
+    {if $coverLetter}
+      {\n}
+      {\n}
+      {$coverLetter}
+    {/if}
+    {if $email.hasInlineComments}
+      {\n}
+      {\n}
+      {$email.inlineComments}
+    {/if}
+  {/if}
+  {\n}
+{/template}
\ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
deleted file mode 100644
index a442311..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
+++ /dev/null
@@ -1,55 +0,0 @@
-## Copyright (C) 2010 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 Comment.vm template will determine the contents of the email related to
-## a user submitting comments on changes.  It is a ChangeEmail: see
-## ChangeSubject.vm, ChangeFooter.vm and CommentFooter.vm.
-##
-#if ($email.coverLetter || $email.hasInlineComments())
-$fromName has posted comments on this change.#**
-*##if($email.changeUrl) ( $email.changeUrl )#end
-
-
-Change subject: $change.subject
-......................................................................
-
-
-#if ($email.coverLetter)
-$email.coverLetter
-
-#end
-##
-## It is possible to increase the span of the quoted lines by using the line
-## count parameter when calling $email.getInlineComments as a function.
-##
-## Example: #if($email.hasInlineComments())$email.getInlineComments(5)#end
-##
-#if($email.hasInlineComments())$email.inlineComments#end
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy
new file mode 100644
index 0000000..3fcad6b
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy
@@ -0,0 +1,31 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .CommentFooter template will determine the contents of the footer text
+ * that will be appended to emails related to a user submitting comments on
+ * changes.
+ * @param email
+ */
+{template .CommentFooter autoescape="strict" kind="text"}
+  {if $email.hasInlineComments}
+    Gerrit-HasComments: Yes
+  {else}
+    Gerrit-HasComments: No
+  {/if}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.vm
deleted file mode 100644
index e0832e6..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.vm
+++ /dev/null
@@ -1,40 +0,0 @@
-## Copyright (C) 2012 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 CommentFooter.vm template will determine the contents of the footer
-## text that will be appended to emails related to a user submitting comments
-## on changes.
-##
-## See ChangeSubject.vm and ChangeFooter.vm.
-#if($email.hasInlineComments())
-Gerrit-HasComments: Yes
-#else
-Gerrit-HasComments: No
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
new file mode 100644
index 0000000..888ee4b
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
@@ -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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .DeleteReviewer template will determine the contents of the email related
+ * to removal of a reviewer (and the reviewer's votes) from reviews.
+ * @param change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .DeleteReviewer autoescape="strict" kind="text"}
+  {$fromName} has removed{sp}
+  {foreach $reviewerName in $email.reviewerNames}
+    {if not isFirst($reviewerName)},{sp}{/if}
+    {$reviewerName}
+  {/foreach}{sp}
+  from this change.{sp}
+  {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
+  {\n}
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {if $coverLetter}
+    {\n}
+    {\n}
+    {$coverLetter}
+    {\n}
+  {/if}
+{/template}
\ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.vm
deleted file mode 100644
index 635b716..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.vm
+++ /dev/null
@@ -1,47 +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.
-##
-##
-## 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 DeleteReviewer.vm template will determine the contents of the email
-## related to removal of a reviewer (and the reviewer's votes) from reviews.
-## It is a ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
-##
-$fromName has removed $email.joinStrings($email.reviewerNames, ', ') from #**
-*#this change.#**
-*##if($email.changeUrl) ( $email.changeUrl )#end
-
-
-Change subject: $change.subject
-......................................................................
-
-
-#if ($email.coverLetter)
-$email.coverLetter
-
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy
new file mode 100644
index 0000000..b249ded
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy
@@ -0,0 +1,37 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .DeleteVote template will determine the contents of the email related
+ * to removing votes on changes.
+ * @param change
+ * @param coverLetter
+ * @param fromName
+ */
+{template .DeleteVote autoescape="strict" kind="text"}
+  {$fromName} has removed a vote on this change.{\n}
+  {\n}
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {if $coverLetter}
+    {\n}
+    {\n}
+    {$coverLetter}
+    {\n}
+  {/if}
+{/template}
\ No newline at end of file
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
deleted file mode 100644
index 294063e..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.vm
+++ /dev/null
@@ -1,44 +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.
-##
-##
-## 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/mail/Footer.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy
new file mode 100644
index 0000000..6467e95
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy
@@ -0,0 +1,25 @@
+/**
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .Footer template will determine the contents of the footer text
+ * appended to the end of all outgoing emails after the ChangeFooter and
+ * CommentFooter.
+ */
+{template .Footer}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm
deleted file mode 100644
index 28f29fd..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm
+++ /dev/null
@@ -1,33 +0,0 @@
-## Copyright (C) 2013 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 Footer.vm template will determine the contents of the footer text
-## appended to the end of all outgoing emails after the ChangeFooter and
-## CommentFooter.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy
new file mode 100644
index 0000000..9befa51
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy
@@ -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.
+*/
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .FooterHtml autoescape="strict" kind="html"}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy
new file mode 100644
index 0000000..d483264
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy
@@ -0,0 +1,42 @@
+
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .Merged template will determine the contents of the email related to
+ * a change successfully merged to the head.
+ * @param change
+ * @param email
+ * @param fromName
+ */
+{template .Merged autoescape="strict" kind="text"}
+  {$fromName} has submitted this change and it was merged.
+  {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
+  {\n}
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {\n}
+  {$email.changeDetail}
+  {$email.approvals}
+  {if $email.includeDiff}
+    {\n}
+    {\n}
+    {$email.unifiedDiff}
+    {\n}
+  {/if}
+{/template}
\ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
deleted file mode 100644
index 3e49e92..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
+++ /dev/null
@@ -1,47 +0,0 @@
-## Copyright (C) 2010 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 Merged.vm template will determine the contents of the email related to
-## a change successfully merged to the head.  It is a ChangeEmail: see
-## ChangeSubject.vm and ChangeFooter.vm.
-##
-$fromName has submitted this change and it was merged.#**
-*##if($email.changeUrl) ( $email.changeUrl )#end
-
-
-Change subject: $change.subject
-......................................................................
-
-
-$email.changeDetail$email.approvals
-
-#if($email.includeDiff)
-$email.UnifiedDiff
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy
new file mode 100644
index 0000000..296f625
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy
@@ -0,0 +1,81 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .NewChange template will determine the contents of the email related to a
+ * user submitting a new change for review.
+ * @param change
+ * @param email
+ * @param fromName
+ * @param patchSet
+ * @param projectName
+ */
+{template .NewChange autoescape="strict" kind="text"}
+  {if $email.reviewerNames}
+    Hello{sp}
+    {foreach $reviewerName in $email.reviewerNames}
+      {if not isFirst($reviewerName)},{sp}{/if}
+      {$reviewerName}
+    {/foreach},
+
+    {\n}
+    {\n}
+
+    I'd like you to do a code review.
+
+    {if $email.changeUrl}
+      {sp}Please visit
+
+      {\n}
+      {\n}
+
+      {sp}{sp}{sp}{sp}{$email.changeUrl}
+
+      {\n}
+      {\n}
+
+      to review the following change.
+    {/if}
+  {else}
+    {$fromName} has uploaded a new change for review.
+    {if $email.changeUrl} ( {$email.changeUrl}{/if}
+  {/if}{\n}
+
+  {\n}
+  {\n}
+
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+
+  {\n}
+
+  {$email.changeDetail}{\n}
+
+  {if $email.sshHost}
+    {\n}
+    {sp}{sp}git pull ssh:{print '//'}{$email.sshHost}/{$projectName}
+        {sp}{$patchSet.refName}
+    {\n}
+  {/if}
+
+  {if $email.includeDiff}
+    {\n}
+    {$email.unifiedDiff}
+    {\n}
+  {/if}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
deleted file mode 100644
index 8b66e81..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
+++ /dev/null
@@ -1,60 +0,0 @@
-## Copyright (C) 2010 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 NewChange.vm template will determine the contents of the email related
-## to a user submitting a new change for review. It is a ChangeEmail: see
-## ChangeSubject.vm and ChangeFooter.vm.
-##
-#if($email.reviewerNames)
-Hello $email.joinStrings($email.reviewerNames, ', '),
-
-I'd like you to do a code review.#if($email.changeUrl)  Please visit
-
-    $email.changeUrl
-
-to review the following change.
-#end
-#else
-$fromName has uploaded a new change for review.#**
-*##if($email.changeUrl) ( $email.changeUrl )#end
-#end
-
-
-Change subject: $change.subject
-......................................................................
-
-$email.changeDetail
-#if($email.sshHost)
-  git pull ssh://$email.sshHost/$projectName $patchSet.refName
-#end
-#if($email.includeDiff)
-
-$email.UnifiedDiff
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy
new file mode 100644
index 0000000..2b30ae6
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy
@@ -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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .RegisterNewEmail template will determine the contents of the email
+ * related to registering new email accounts.
+ * @param email
+ */
+{template .RegisterNewEmail autoescape="strict" kind="text"}
+  Welcome to Gerrit Code Review at {$email.gerritHost}.{\n}
+
+  {\n}
+
+  To add a verified email address to your user account, please{\n}
+  click on the following link
+  {if $email.userNameEmail}
+    {sp}while signed in as {$email.userNameEmail}
+  {/if}:{\n}
+
+  {\n}
+
+  {$email.gerritUrl}#/VE/{$email.emailRegistrationToken}{\n}
+
+  {\n}
+
+  If you have received this mail in error, you do not need to take any{\n}
+  action to cancel the account.  The address will not be activated, and{\n}
+  you will not receive any further emails.{\n}
+
+  {\n}
+
+  If clicking the link above does not work, copy and paste the URL in a{\n}
+  new browser window instead.{\n}
+
+  {\n}
+
+  This is a send-only email address.  Replies to this message will not{\n}
+  be read or answered.{\n}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm
deleted file mode 100644
index 7e095fb..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm
+++ /dev/null
@@ -1,49 +0,0 @@
-## Copyright (C) 2010 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 RegisterNewEmail.vm template will determine the contents of the email
-## related to registering new email accounts.
-##
-Welcome to Gerrit Code Review at ${email.gerritHost}.
-
-To add a verified email address to your user account, please
-click on the following link#if($email.userNameEmail) while signed in as $email.userNameEmail#end:
-
-$email.gerritUrl#/VE/$email.emailRegistrationToken
-
-If you have received this mail in error, you do not need to take any
-action to cancel the account.  The address will not be activated, and
-you will not receive any further emails.
-
-If clicking the link above does not work, copy and paste the URL in a
-new browser window instead.
-
-This is a send-only email address.  Replies to this message will not
-be read or answered.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
new file mode 100644
index 0000000..2236725
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
@@ -0,0 +1,59 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .ReplacePatchSet template will determine the contents of the email
+ * related to a user submitting a new patchset for a change.
+ * @param change
+ * @param email
+ * @param fromName
+ * @param patchSet
+ * @param projectName
+ */
+{template .ReplacePatchSet autoescape="strict" kind="text"}
+  {if $email.reviewerNames}
+    Hello{sp}
+    {foreach $reviewerName in $email.reviewerNames}
+      {$reviewerName},{sp}
+    {/foreach}{\n}
+    {\n}
+    I'd like you to reexamine a change.
+    {if $email.changeUrl}
+      {sp}Please visit
+      {\n}
+      {\n}
+      {sp}{sp}{sp}{sp}{$email.changeUrl}
+      {\n}
+      {\n}
+      to look at the new patch set (#{$patchSet.patchSetId}).
+    {/if}
+  {else}
+    {$fromName} has uploaded a new patch set (#{$patchSet.patchSetId}).
+    {if $email.changeUrl} ( {$email.changeUrl}{/if}
+  {/if}{\n}
+  {\n}
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {\n}
+  {$email.changeDetail}{\n}
+  {if $email.sshHost}
+    {sp}{sp}git pull ssh:{print '//'}{$email.sshHost}/{$projectName}{sp}
+        {$patchSet.refName}
+    {\n}
+  {/if}
+{/template}
\ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm
deleted file mode 100644
index e45bf30..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm
+++ /dev/null
@@ -1,56 +0,0 @@
-## Copyright (C) 2010 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 ReplacePatchSet.vm template will determine the contents of the email
-## related to a user submitting a new patchset for a change.  It is a
-## ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
-##
-#if($email.reviewerNames)
-Hello $email.joinStrings($email.reviewerNames, ', '),
-
-I'd like you to reexamine a change.#if($email.changeUrl)  Please visit
-
-    $email.changeUrl
-
-to look at the new patch set (#$patchSet.patchSetId).
-#end
-#else
-$fromName has uploaded a new patch set (#$patchSet.patchSetId).#**
-*##if($email.changeUrl) ( $email.changeUrl )#end
-
-#end
-
-Change subject: $change.subject
-......................................................................
-
-$email.changeDetail
-#if($email.sshHost)
-  git pull ssh://$email.sshHost/$projectName $patchSet.refName
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy
new file mode 100644
index 0000000..14ae0f3
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy
@@ -0,0 +1,39 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .Restored template will determine the contents of the email related to a
+ * change being restored.
+ * @param change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .Restored autoescape="strict" kind="text"}
+  {$fromName} has restored this change.
+  {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
+  {\n}
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {if $coverLetter}
+    {\n}
+    {\n}
+    {$coverLetter}
+    {\n}
+  {/if}
+{/template}
\ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm
deleted file mode 100644
index 31e1c69..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm
+++ /dev/null
@@ -1,46 +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.
-##
-##
-## 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 Restored.vm template will determine the contents of the email related
-## to a change being restored.   It is a ChangeEmail: see ChangeSubject.vm and
-## ChangeFooter.vm.
-##
-$fromName has restored this change.#**
-*##if($email.changeUrl) ( $email.changeUrl )#end
-
-
-Change subject: $change.subject
-......................................................................
-
-
-#if ($coverLetter)
-$coverLetter
-
-#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy
new file mode 100644
index 0000000..7f74df9
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy
@@ -0,0 +1,39 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * The .Reverted template will determine the contents of the email related
+ * to a change being reverted.
+ * @param change
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .Reverted autoescape="strict" kind="text"}
+  {$fromName} has reverted this change.
+  {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
+  {\n}
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {if $coverLetter}
+    {\n}
+    {\n}
+    {$coverLetter}
+    {\n}
+  {/if}
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm
deleted file mode 100644
index 1e9e251..0000000
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm
+++ /dev/null
@@ -1,46 +0,0 @@
-## Copyright (C) 2010 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 Reverted.vm template will determine the contents of the email related
-## to a change being reverted.   It is a ChangeEmail: see ChangeSubject.vm and
-## ChangeFooter.vm.
-##
-$fromName has reverted this change.#**
-*##if($email.changeUrl) ( $email.changeUrl )#end
-
-
-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 d51547c..5a937b6 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
@@ -97,7 +97,7 @@
 in = text/x-properties
 ini = text/x-properties
 intr = text/x-dylan
-jade = text/x-jade
+jade = text/x-pug
 java = text/x-java
 jl = text/x-julia
 jruby = text/x-ruby
@@ -163,6 +163,7 @@
 ps1 = application/x-powershell
 psd1 = application/x-powershell
 psm1 = application/x-powershell
+pug = text/x-pug
 py = text/x-python
 pyw = text/x-python
 pyx = text/x-cython
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
index 11f1d54..87dd3d9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -35,8 +35,10 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Set;
 
 public class FromAddressGeneratorProviderTest {
@@ -60,6 +62,10 @@
     config.setString("sendemail", null, "from", newFrom);
   }
 
+  private void setDomains(List<String> domains) {
+    config.setStringList("sendemail", null, "allowedDomain", domains);
+  }
+
   @Test
   public void testDefaultIsMIXED() {
     assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class);
@@ -118,7 +124,7 @@
     replay(accountCache);
     final Address r = create().from(user);
     assertThat(r).isNotNull();
-    assertThat(r.name).isEqualTo(name);
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
     assertThat(r.email).isEqualTo(ident.getEmailAddress());
     verify(accountCache);
   }
@@ -135,6 +141,88 @@
   }
 
   @Test
+  public void testUSERAllowDomain() {
+    setFrom("USER");
+    setDomains(Arrays.asList("*.example.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.example.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERNoAllowDomain() {
+    setFrom("USER");
+    setDomains(Arrays.asList("example.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERAllowDomainTwice() {
+    setFrom("USER");
+    setDomains(Arrays.asList("example.com"));
+    setDomains(Arrays.asList("test.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERAllowDomainTwiceReverse() {
+    setFrom("USER");
+    setDomains(Arrays.asList("test.com"));
+    setDomains(Arrays.asList("example.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name + " (Code Review)");
+    assertThat(r.email).isEqualTo(ident.getEmailAddress());
+    verify(accountCache);
+  }
+
+  @Test
+  public void testUSERAllowTwoDomains() {
+    setFrom("USER");
+    setDomains(Arrays.asList("example.com", "test.com"));
+    final String name = "A U. Thor";
+    final String email = "a.u.thor@test.com";
+    final Account.Id user = user(name, email);
+
+    replay(accountCache);
+    final Address r = create().from(user);
+    assertThat(r).isNotNull();
+    assertThat(r.name).isEqualTo(name);
+    assertThat(r.email).isEqualTo(email);
+    verify(accountCache);
+  }
+
+  @Test
   public void testSelectSERVER() {
     setFrom("SERVER");
     assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class);
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 0173b05..a29d397 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
@@ -322,7 +322,10 @@
     update.commit();
 
     notes = newNotes(c);
-    assertThat(notes.getApprovals()).isEmpty();
+    assertThat(notes.getApprovals()).containsExactlyEntriesIn(
+        ImmutableMultimap.of(
+            psa.getPatchSetId(),
+            new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen())));
   }
 
   @Test
@@ -344,7 +347,10 @@
     update.commit();
 
     notes = newNotes(c);
-    assertThat(notes.getApprovals()).isEmpty();
+    assertThat(notes.getApprovals()).containsExactlyEntriesIn(
+        ImmutableMultimap.of(
+            psa.getPatchSetId(),
+            new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen())));
 
     // Add back approval on same label.
     update = newUpdate(c, otherUser);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index d4d77bd..596ed59 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -137,13 +137,13 @@
   }
 
   private void assertCanSubmit(String ref, ProjectControl u) {
-    assertThat(u.controlForRef(ref).canSubmit())
+    assertThat(u.controlForRef(ref).canSubmit(false))
       .named("can submit " + ref)
       .isTrue();
   }
 
   private void assertCannotSubmit(String ref, ProjectControl u) {
-    assertThat(u.controlForRef(ref).canSubmit())
+    assertThat(u.controlForRef(ref).canSubmit(false))
       .named("can submit " + ref)
       .isFalse();
   }
@@ -685,6 +685,43 @@
   }
 
   @Test
+  public void testUnblockMoreSpecificRefInLocal_Fails() {
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, PUSH, DEVS, "refs/heads/master");
+
+    ProjectControl u = user(local, DEVS);
+    assertCannotUpdate("refs/heads/master", u);
+  }
+
+  @Test
+  public void testUnblockMoreSpecificRefWithExclusiveFlag() {
+    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, PUSH, DEVS, "refs/heads/master", true);
+
+    ProjectControl u = user(local, DEVS);
+    assertCanUpdate("refs/heads/master", u);
+  }
+
+  @Test
+  public void testUnblockMoreSpecificRefInLocalWithExclusiveFlag_Fails() {
+    block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, PUSH, DEVS, "refs/heads/master", true);
+
+    ProjectControl u = user(local, DEVS);
+    assertCannotUpdate("refs/heads/master", u);
+  }
+
+  @Test
+  public void testUnblockOtherPermissionWithMoreSpecificRefAndExclusiveFlag_Fails() {
+    block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
+    allow(local, PUSH, DEVS, "refs/heads/master");
+    allow(local, SUBMIT, DEVS, "refs/heads/master", true);
+
+    ProjectControl u = user(local, DEVS);
+    assertCannotUpdate("refs/heads/master", u);
+  }
+
+  @Test
   public void testUnblockLargerScope_Fails() {
     block(local, PUSH, ANONYMOUS_USERS, "refs/heads/master");
     allow(local, PUSH, DEVS, "refs/heads/*");
@@ -825,6 +862,14 @@
   }
 
   @Test
+  public void testBlockOwner() {
+    block(parent, OWNER, ANONYMOUS_USERS, "refs/*");
+    allow(local, OWNER, DEVS, "refs/*");
+
+    assertThat(user(local, DEVS).isOwner()).isFalse();
+  }
+
+  @Test
   public void testValidateRefPatternsOK() throws Exception {
     RefPattern.validate("refs/*");
     RefPattern.validate("^refs/heads/*");
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
index 772c778..9cbdae2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
@@ -97,6 +97,13 @@
   }
 
   public static PermissionRule allow(ProjectConfig project,
+      String permissionName, AccountGroup.UUID group, String ref,
+      boolean exclusive) {
+    return grant(project, permissionName, newRule(project, group), ref,
+        exclusive);
+  }
+
+  public static PermissionRule allow(ProjectConfig project,
       String capabilityName, AccountGroup.UUID group) {
     PermissionRule rule = newRule(project, group);
     project.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
@@ -163,9 +170,18 @@
 
   private static PermissionRule grant(ProjectConfig project,
       String permissionName, PermissionRule rule, String ref) {
-    project.getAccessSection(ref, true) //
-        .getPermission(permissionName, true) //
-        .add(rule);
+    return grant(project, permissionName, rule, ref, false);
+  }
+
+  private static PermissionRule grant(ProjectConfig project,
+      String permissionName, PermissionRule rule, String ref,
+      boolean exclusive) {
+    Permission permission = project.getAccessSection(ref, true)
+        .getPermission(permissionName, true);
+    if (exclusive) {
+      permission.setExclusiveGroup(exclusive);
+    }
+    permission.add(rule);
     return rule;
   }
 
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 0c658bf..c5221f1 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
@@ -37,6 +37,7 @@
 import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.HashtagsInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.StarsInput;
 import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -1583,7 +1584,7 @@
 
     PatchSetInserter inserter = patchSetFactory.create(
           ctl, new PatchSet.Id(c.getId(), n), commit)
-        .setSendMail(false)
+        .setNotify(NotifyHandling.NONE)
         .setFireRevisionCreated(false)
         .setValidatePolicy(CommitValidators.Policy.NONE);
     try (BatchUpdate bu = updateFactory.create(
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 9e5b776..5fb930c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -18,11 +18,11 @@
 
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index ac64803..bffb114 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -111,7 +112,7 @@
       ReviewerResource rsrc = reviewerFactory.create(changeRsrc, reviewer);
       String error = null;
       try {
-        deleteReviewer.apply(rsrc, new DeleteReviewer.Input());
+        deleteReviewer.apply(rsrc, new DeleteReviewerInput());
       } catch (ResourceNotFoundException e) {
         error = String.format("could not remove %s: not found", reviewer);
       } catch (Exception e) {
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index f5b078e..acc9b86 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>2.13-SNAPSHOT</version>
+  <version>2.14-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 71eed63..5790453 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.Splitter;
 import com.google.gerrit.common.EventBroker;
+import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.gpg.GpgModule;
 import com.google.gerrit.httpd.auth.oauth.OAuthModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
@@ -29,7 +30,6 @@
 import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
 import com.google.gerrit.pgm.util.LogFileCompressor;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.InternalAccountDirectory;
 import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.change.ChangeCleanupRunner;
diff --git a/lib/BUCK b/lib/BUCK
index ecd30ae..6e843c4 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -17,6 +17,7 @@
 define_license(name = 'fetch')
 define_license(name = 'h2')
 define_license(name = 'highlightjs')
+define_license(name = 'icu4j')
 define_license(name = 'jgit')
 define_license(name = 'jsch')
 define_license(name = 'MPL1.1')
@@ -90,7 +91,11 @@
   # Whitelist lib targets that have jsr305 as a dependency. Generally speaking
   # Gerrit core should not depend on these annotations, and instead use
   # equivalent annotations in com.google.gerrit.common.
-  visibility = ['//lib:guava-retrying'],
+  visibility = [
+    '//gerrit-plugin-api:lib',
+    '//lib:guava-retrying',
+    '//lib:soy',
+  ],
 )
 
 maven_jar(
@@ -273,3 +278,34 @@
   license = 'Apache2.0',
   repository = GERRIT,
 )
+
+# Keep this version of Soy synchronized with the version used in Gitiles.
+maven_jar(
+  name = 'soy',
+  id = 'com.google.template:soy:2016-08-09',
+  sha1 = '43d33651e95480d515fe26c10a662faafe3ad1e4',
+  license = 'Apache2.0',
+  deps = [
+    ':args4j',
+    ':guava',
+    ':gson',
+    ':icu4j',
+    ':jsr305',
+    ':protobuf',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:multibindings',
+    '//lib/guice:javax-inject',
+    '//lib/ow2:ow2-asm',
+    '//lib/ow2:ow2-asm-analysis',
+    '//lib/ow2:ow2-asm-commons',
+    '//lib/ow2:ow2-asm-util',
+  ],
+)
+
+maven_jar(
+  name = 'icu4j',
+  id = 'com.ibm.icu:icu4j:57.1',
+  sha1 = '198ea005f41219f038f4291f0b0e9f3259730e92',
+  license = 'icu4j',
+)
diff --git a/lib/BUILD b/lib/BUILD
index e89e63c..a490038 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -151,6 +151,7 @@
   visibility = ['//visibility:public'],
 )
 
+
 java_library(
   name = 'h2',
   exports = ['@h2//jar'],
@@ -202,3 +203,31 @@
   exports = ['@derby//jar'],
   visibility = ['//visibility:public'],
 )
+
+java_library(
+  name = 'soy',
+  exports = ['@soy//jar'],
+  runtime_deps = [
+    ':args4j',
+    ':guava',
+    ':gson',
+    ':icu4j',
+    ':jsr305',
+    ':protobuf',
+    '//lib/guice:guice',
+    '//lib/guice:guice-assistedinject',
+    '//lib/guice:multibindings',
+    '//lib/guice:javax-inject',
+    '//lib/ow2:ow2-asm',
+    '//lib/ow2:ow2-asm-analysis',
+    '//lib/ow2:ow2-asm-commons',
+    '//lib/ow2:ow2-asm-util',
+  ],
+  visibility = ['//visibility:public'],
+)
+
+java_library(
+  name = 'icu4j',
+  exports = [ '@icu4j//jar' ],
+  visibility = ['//visibility:public'],
+)
diff --git a/lib/LICENSE-icu4j b/lib/LICENSE-icu4j
new file mode 100644
index 0000000..90be7cd
--- /dev/null
+++ b/lib/LICENSE-icu4j
@@ -0,0 +1,385 @@
+COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later)
+
+Copyright © 1991-2016 Unicode, Inc. All rights reserved.
+Distributed under the Terms of Use in http://www.unicode.org/copyright.html
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Unicode data files and any associated documentation
+(the "Data Files") or Unicode software and any associated documentation
+(the "Software") to deal in the Data Files or Software
+without restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, and/or sell copies of
+the Data Files or Software, and to permit persons to whom the Data Files
+or Software are furnished to do so, provided that either
+(a) this copyright and permission notice appear with all copies
+of the Data Files or Software, or
+(b) this copyright and permission notice appear in associated
+Documentation.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT OF THIRD PARTY RIGHTS.
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
+NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
+DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
+DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THE DATA FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale,
+use or other dealings in these Data Files or Software without prior
+written authorization of the copyright holder.
+
+---------------------
+
+Third-Party Software Licenses
+
+This section contains third-party software notices and/or additional
+terms for licensed third-party software components included within ICU
+libraries.
+
+1. ICU License - ICU 1.8.1 to ICU 57.1
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright (c) 1995-2016 International Business Machines Corporation and others
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, provided that the above
+copyright notice(s) and this permission notice appear in all copies of
+the Software and that both the above copyright notice(s) and this
+permission notice appear in supporting documentation.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY
+SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
+RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder
+shall not be used in advertising or otherwise to promote the sale, use
+or other dealings in this Software without prior written authorization
+of the copyright holder.
+
+All trademarks and registered trademarks mentioned herein are the
+property of their respective owners.
+
+2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt)
+
+ #     The Google Chrome software developed by Google is licensed under
+ # the BSD license. Other software included in this distribution is
+ # provided under other licenses, as set forth below.
+ #
+ #  The BSD License
+ #  http://opensource.org/licenses/bsd-license.php
+ #  Copyright (C) 2006-2008, Google Inc.
+ #
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ # modification, are permitted provided that the following conditions are met:
+ #
+ #  Redistributions of source code must retain the above copyright notice,
+ # this list of conditions and the following disclaimer.
+ #  Redistributions in binary form must reproduce the above
+ # copyright notice, this list of conditions and the following
+ # disclaimer in the documentation and/or other materials provided with
+ # the distribution.
+ #  Neither the name of  Google Inc. nor the names of its
+ # contributors may be used to endorse or promote products derived from
+ # this software without specific prior written permission.
+ #
+ #
+ #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ #
+ #
+ #  The word list in cjdict.txt are generated by combining three word lists
+ # listed below with further processing for compound word breaking. The
+ # frequency is generated with an iterative training against Google web
+ # corpora.
+ #
+ #  * Libtabe (Chinese)
+ #    - https://sourceforge.net/project/?group_id=1519
+ #    - Its license terms and conditions are shown below.
+ #
+ #  * IPADIC (Japanese)
+ #    - http://chasen.aist-nara.ac.jp/chasen/distribution.html
+ #    - Its license terms and conditions are shown below.
+ #
+ #  ---------COPYING.libtabe ---- BEGIN--------------------
+ #
+ #  /*
+ #   * Copyrighy (c) 1999 TaBE Project.
+ #   * Copyright (c) 1999 Pai-Hsiang Hsiao.
+ #   * All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the TaBE Project nor the names of its
+ #   *   contributors may be used to endorse or promote products derived
+ #   *   from this software without specific prior written permission.
+ #   *
+ #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ #   * OF THE POSSIBILITY OF SUCH DAMAGE.
+ #   */
+ #
+ #  /*
+ #   * Copyright (c) 1999 Computer Systems and Communication Lab,
+ #   *                    Institute of Information Science, Academia
+ #       *                    Sinica. All rights reserved.
+ #   *
+ #   * Redistribution and use in source and binary forms, with or without
+ #   * modification, are permitted provided that the following conditions
+ #   * are met:
+ #   *
+ #   * . Redistributions of source code must retain the above copyright
+ #   *   notice, this list of conditions and the following disclaimer.
+ #   * . Redistributions in binary form must reproduce the above copyright
+ #   *   notice, this list of conditions and the following disclaimer in
+ #   *   the documentation and/or other materials provided with the
+ #   *   distribution.
+ #   * . Neither the name of the Computer Systems and Communication Lab
+ #   *   nor the names of its contributors may be used to endorse or
+ #   *   promote products derived from this software without specific
+ #   *   prior written permission.
+ #   *
+ #   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ #   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ #   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ #   * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ #   * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ #   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ #   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ #   * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ #   * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ #   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ #   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ #   * OF THE POSSIBILITY OF SUCH DAMAGE.
+ #   */
+ #
+ #  Copyright 1996 Chih-Hao Tsai @ Beckman Institute,
+ #      University of Illinois
+ #  c-tsai4@uiuc.edu  http://casper.beckman.uiuc.edu/~c-tsai4
+ #
+ #  ---------------COPYING.libtabe-----END--------------------------------
+ #
+ #
+ #  ---------------COPYING.ipadic-----BEGIN-------------------------------
+ #
+ #  Copyright 2000, 2001, 2002, 2003 Nara Institute of Science
+ #  and Technology.  All Rights Reserved.
+ #
+ #  Use, reproduction, and distribution of this software is permitted.
+ #  Any copy of this software, whether in its original form or modified,
+ #  must include both the above copyright notice and the following
+ #  paragraphs.
+ #
+ #  Nara Institute of Science and Technology (NAIST),
+ #  the copyright holders, disclaims all warranties with regard to this
+ #  software, including all implied warranties of merchantability and
+ #  fitness, in no event shall NAIST be liable for
+ #  any special, indirect or consequential damages or any damages
+ #  whatsoever resulting from loss of use, data or profits, whether in an
+ #  action of contract, negligence or other tortuous action, arising out
+ #  of or in connection with the use or performance of this software.
+ #
+ #  A large portion of the dictionary entries
+ #  originate from ICOT Free Software.  The following conditions for ICOT
+ #  Free Software applies to the current dictionary as well.
+ #
+ #  Each User may also freely distribute the Program, whether in its
+ #  original form or modified, to any third party or parties, PROVIDED
+ #  that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear
+ #  on, or be attached to, the Program, which is distributed substantially
+ #  in the same form as set out herein and that such intended
+ #  distribution, if actually made, will neither violate or otherwise
+ #  contravene any of the laws and regulations of the countries having
+ #  jurisdiction over the User or the intended distribution itself.
+ #
+ #  NO WARRANTY
+ #
+ #  The program was produced on an experimental basis in the course of the
+ #  research and development conducted during the project and is provided
+ #  to users as so produced on an experimental basis.  Accordingly, the
+ #  program is provided without any warranty whatsoever, whether express,
+ #  implied, statutory or otherwise.  The term "warranty" used herein
+ #  includes, but is not limited to, any warranty of the quality,
+ #  performance, merchantability and fitness for a particular purpose of
+ #  the program and the nonexistence of any infringement or violation of
+ #  any right of any third party.
+ #
+ #  Each user of the program will agree and understand, and be deemed to
+ #  have agreed and understood, that there is no warranty whatsoever for
+ #  the program and, accordingly, the entire risk arising from or
+ #  otherwise connected with the program is assumed by the user.
+ #
+ #  Therefore, neither ICOT, the copyright holder, or any other
+ #  organization that participated in or was otherwise related to the
+ #  development of the program and their respective officials, directors,
+ #  officers and other employees shall be held liable for any and all
+ #  damages, including, without limitation, general, special, incidental
+ #  and consequential damages, arising out of or otherwise in connection
+ #  with the use or inability to use the program or any product, material
+ #  or result produced or otherwise obtained by using the program,
+ #  regardless of whether they have been advised of, or otherwise had
+ #  knowledge of, the possibility of such damages at any time during the
+ #  project or thereafter.  Each user will be deemed to have agreed to the
+ #  foregoing by his or her commencement of use of the program.  The term
+ #  "use" as used herein includes, but is not limited to, the use,
+ #  modification, copying and distribution of the program and the
+ #  production of secondary products from the program.
+ #
+ #  In the case where the program, whether in its original form or
+ #  modified, was distributed or delivered to or received by a user from
+ #  any person, organization or entity other than ICOT, unless it makes or
+ #  grants independently of ICOT any specific warranty to the user in
+ #  writing, such person, organization or entity, will also be exempted
+ #  from and not be held liable to the user for any such damages as noted
+ #  above as far as the program is concerned.
+ #
+ #  ---------------COPYING.ipadic-----END----------------------------------
+
+3. Lao Word Break Dictionary Data (laodict.txt)
+
+ #  Copyright (c) 2013 International Business Machines Corporation
+ #  and others. All Rights Reserved.
+ #
+ # Project: http://code.google.com/p/lao-dictionary/
+ # Dictionary: http://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt
+ # License: http://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt
+ #              (copied below)
+ #
+ #  This file is derived from the above dictionary, with slight
+ #  modifications.
+ #  ----------------------------------------------------------------------
+ #  Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell.
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ #  modification,
+ #  are permitted provided that the following conditions are met:
+ #
+ #
+ # Redistributions of source code must retain the above copyright notice, this
+ #  list of conditions and the following disclaimer. Redistributions in
+ #  binary form must reproduce the above copyright notice, this list of
+ #  conditions and the following disclaimer in the documentation and/or
+ #  other materials provided with the distribution.
+ #
+ #
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ # OF THE POSSIBILITY OF SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+4. Burmese Word Break Dictionary Data (burmesedict.txt)
+
+ #  Copyright (c) 2014 International Business Machines Corporation
+ #  and others. All Rights Reserved.
+ #
+ #  This list is part of a project hosted at:
+ #    github.com/kanyawtech/myanmar-karen-word-lists
+ #
+ #  --------------------------------------------------------------------------
+ #  Copyright (c) 2013, LeRoy Benjamin Sharon
+ #  All rights reserved.
+ #
+ #  Redistribution and use in source and binary forms, with or without
+ #  modification, are permitted provided that the following conditions
+ #  are met: Redistributions of source code must retain the above
+ #  copyright notice, this list of conditions and the following
+ #  disclaimer.  Redistributions in binary form must reproduce the
+ #  above copyright notice, this list of conditions and the following
+ #  disclaimer in the documentation and/or other materials provided
+ #  with the distribution.
+ #
+ #    Neither the name Myanmar Karen Word Lists, nor the names of its
+ #    contributors may be used to endorse or promote products derived
+ #    from this software without specific prior written permission.
+ #
+ #  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ #  CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ #  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ #  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ #  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
+ #  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ #  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ #  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ #  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ #  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+ #  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ #  THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ #  SUCH DAMAGE.
+ #  --------------------------------------------------------------------------
+
+5. Time Zone Database
+
+  ICU uses the public domain data and code derived from Time Zone
+Database for its time zone support. The ownership of the TZ database
+is explained in BCP 175: Procedure for Maintaining the Time Zone
+Database section 7.
+
+ # 7.  Database Ownership
+ #
+ #    The TZ database itself is not an IETF Contribution or an IETF
+ #    document.  Rather it is a pre-existing and regularly updated work
+ #    that is in the public domain, and is intended to remain in the
+ #    public domain.  Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do
+ #    not apply to the TZ Database or contributions that individuals make
+ #    to it.  Should any claims be made and substantiated against the TZ
+ #    Database, the organization that is providing the IANA
+ #    Considerations defined in this RFC, under the memorandum of
+ #    understanding with the IETF, currently ICANN, may act in accordance
+ #    with all competent court orders.  No ownership claims will be made
+ #    by ICANN or the IETF Trust on the database or the code.  Any person
+ #    making a contribution to the database or code waives all rights to
+ #    future claims in that contribution or in the TZ Database.
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK
index a0e0e9a..56145ea 100644
--- a/lib/codemirror/BUCK
+++ b/lib/codemirror/BUCK
@@ -1,14 +1,14 @@
 include_defs('//lib/maven.defs')
 include_defs('//lib/codemirror/cm.defs')
 
-VERSION = '5.17.0'
+VERSION = '5.18.2'
 TOP = 'META-INF/resources/webjars/codemirror/%s' % VERSION
 TOP_MINIFIED = 'META-INF/resources/webjars/codemirror-minified/%s' % VERSION
 
 maven_jar(
   name = 'codemirror-minified',
   id = 'org.webjars.npm:codemirror-minified:' + VERSION,
-  sha1 = '05ad901fc9be67eb7ba8997d896488093deb898e',
+  sha1 = '6755af157a7eaf2401468906bef67bbacc3c97f6',
   attach_source = False,
   license = 'codemirror-minified',
   visibility = [],
@@ -17,7 +17,7 @@
 maven_jar(
   name = 'codemirror-original',
   id = 'org.webjars.npm:codemirror:' + VERSION,
-  sha1 = 'c025b8d9aca1061e26d1fa482bea0ecea1412e85',
+  sha1 = '18c721ae88eed27cddb458c42f5d221fa3d9713e',
   attach_source = False,
   license = 'codemirror-original',
   visibility = [],
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs
index baf2ce5..a1be90f 100644
--- a/lib/codemirror/cm.defs
+++ b/lib/codemirror/cm.defs
@@ -132,7 +132,6 @@
   'htmlmixed',
   'http',
   'idl',
-  'jade',
   'javascript',
   'jinja2',
   'jsx',
@@ -160,6 +159,7 @@
   'powershell',
   'properties',
   'protobuf',
+  'pug',
   'puppet',
   'python',
   'q',
diff --git a/lib/guice/BUCK b/lib/guice/BUCK
index 867b521..8022ac8 100644
--- a/lib/guice/BUCK
+++ b/lib/guice/BUCK
@@ -12,6 +12,7 @@
   exported_deps = [
     ':guice_library',
     ':javax-inject',
+    ':multibindings',
   ],
   visibility = ['PUBLIC'],
 )
@@ -63,3 +64,16 @@
   license = 'Apache2.0',
   visibility = ['PUBLIC'],
 )
+
+maven_jar(
+  name = 'multibindings',
+  id = 'com.google.inject.extensions:guice-multibindings:' + VERSION,
+  sha1 = '3b27257997ac51b0f8d19676f1ea170427e86d51',
+  exclude_java_sources = True,
+  exclude = EXCLUDE + [
+    'META-INF/maven/com.google.guava/guava/pom.properties',
+    'META-INF/maven/com.google.guava/guava/pom.xml',
+  ],
+  license = 'Apache2.0',
+  visibility = ['PUBLIC']
+)
diff --git a/lib/guice/BUILD b/lib/guice/BUILD
index acade50..5850af2 100644
--- a/lib/guice/BUILD
+++ b/lib/guice/BUILD
@@ -3,6 +3,7 @@
   exports = [
     ':guice_library',
     ':javax-inject',
+    ':multibindings',
   ],
   visibility = ['//visibility:public'],
 )
@@ -36,4 +37,11 @@
 java_library(
   name = 'javax-inject',
   exports = ['@javax_inject//jar'],
+  visibility = ['//visibility:public'],
+)
+
+java_library(
+  name = 'multibindings',
+  exports = [ '@multibindings//jar' ],
+  visibility = ['//visibility:public'],
 )
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index c4a9872..dee8ce8 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,6 +1,6 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '5.5.0'
+VERSION = '5.5.2'
 
 # core and backward-codecs both provide
 # META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged.
@@ -16,7 +16,7 @@
 maven_jar(
   name = 'lucene-core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = 'a74fd869bb5ad7fe6b4cd29df9543a34aea81164',
+  sha1 = 'de5e5c3161ea01e89f2a09a14391f9b7ed66cdbb',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -28,7 +28,7 @@
 maven_jar(
   name = 'lucene-analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = '1e0e8243a4410be20c34683034fafa7bb52e55cc',
+  sha1 = 'f0bc3114a6b43f8e64a33c471d5b9e8ddc51564d',
   license = 'Apache2.0',
   deps = [':lucene-core-and-backward-codecs'],
   exclude = [
@@ -40,7 +40,7 @@
 maven_jar(
   name = 'backward-codecs_jar',
   id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
-  sha1 = '68480974b2f54f519763632a7c1c5d51cbff3805',
+  sha1 = 'c5cfcd7a8cf48a0144b61fb991c8e50a0bf868d5',
   license = 'Apache2.0',
   deps = [':lucene-core'],
   exclude = [
@@ -53,7 +53,7 @@
 maven_jar(
   name = 'lucene-misc',
   id = 'org.apache.lucene:lucene-misc:' + VERSION,
-  sha1 = '504d855a1a38190622fdf990b2298c067e7d60ca',
+  sha1 = '37bbe5a2fb429499dfbe75d750d1778881fff45d',
   license = 'Apache2.0',
   deps = [':lucene-core-and-backward-codecs'],
   exclude = [
@@ -65,7 +65,7 @@
 maven_jar(
   name = 'lucene-queryparser',
   id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = '0fddc49725b562fd48dff0cff004336ad2a090a4',
+  sha1 = '8ac921563e744463605284c6d9d2d95e1be5b87c',
   license = 'Apache2.0',
   deps = [':lucene-core-and-backward-codecs'],
   exclude = [
diff --git a/lib/ow2/BUCK b/lib/ow2/BUCK
index fabcb25..653bd2b 100644
--- a/lib/ow2/BUCK
+++ b/lib/ow2/BUCK
@@ -1,25 +1,25 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '5.0.3'
+VERSION = '5.1'
 
 maven_jar(
   name = 'ow2-asm',
   id = 'org.ow2.asm:asm:' + VERSION,
-  sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa',
+  sha1 = '5ef31c4fe953b1fd00b8a88fa1d6820e8785bb45',
   license = 'ow2',
 )
 
 maven_jar(
   name = 'ow2-asm-analysis',
   id = 'org.ow2.asm:asm-analysis:' + VERSION,
-  sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3',
+  sha1 = '6d1bf8989fc7901f868bee3863c44f21aa63d110',
   license = 'ow2',
 )
 
 maven_jar(
   name = 'ow2-asm-commons',
   id = 'org.ow2.asm:asm-commons:' + VERSION,
-  sha1 = 'a7111830132c7f87d08fe48cb0ca07630f8cb91c',
+  sha1 = '25d8a575034dd9cfcb375a39b5334f0ba9c8474e',
   deps = [':ow2-asm-tree'],
   license = 'ow2',
 )
@@ -27,14 +27,13 @@
 maven_jar(
   name = 'ow2-asm-tree',
   id = 'org.ow2.asm:asm-tree:' + VERSION,
-  sha1 = '287749b48ba7162fb67c93a026d690b29f410bed',
+  sha1 = '87b38c12a0ea645791ead9d3e74ae5268d1d6c34',
   license = 'ow2',
 )
 
 maven_jar(
   name = 'ow2-asm-util',
   id = 'org.ow2.asm:asm-util:' + VERSION,
-  sha1 = '1512e5571325854b05fb1efce1db75fcced54389',
+  sha1 = 'b60e33a6bd0d71831e0c249816d01e6c1dd90a47',
   license = 'ow2',
 )
-
diff --git a/plugins/download-commands b/plugins/download-commands
index 7b41f3a..3fb4fb6 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit 7b41f3a413b46140b050ae5324cbbcdd467d2b3a
+Subproject commit 3fb4fb63317b6004761d1fea98a8f4d288d95409
diff --git a/plugins/hooks b/plugins/hooks
index 3acc14d..c1705a7 160000
--- a/plugins/hooks
+++ b/plugins/hooks
@@ -1 +1 @@
-Subproject commit 3acc14d10d26678eae6489038fe0d4dad644a9b4
+Subproject commit c1705a739f117b9123e1d63aebf07d043afb0867
diff --git a/plugins/replication b/plugins/replication
index c5123d6..5cac325 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit c5123d6a5604cc740d6f42485235c0d3ec141c4e
+Subproject commit 5cac325cca171205130c53df8b3ee9ab3b115979
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 3f3d572..46079ec 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 3f3d572e9618f268b19cc54856deee4c96180e4c
+Subproject commit 46079ec92478ddc1e9ffd84eae22fb6af788c9fd
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 90b2e1d..a43f564 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
@@ -108,11 +108,15 @@
     },
 
     _computeProjectURL: function(project) {
-      return '/q/status:open+project:' + project;
+      // @see Issue 4255.
+      return '/q/status:open+project:' +
+          encodeURIComponent(encodeURIComponent(project));
     },
 
     _computeProjectBranchURL: function(project, branch) {
-      return '/q/status:open+project:' + project + '+branch:' + branch;
+      // @see Issue 4255.
+      return this._computeProjectURL(project) +
+          '+branch:' + encodeURIComponent(encodeURIComponent(branch));
     },
   });
 })();
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 b7c0853..d2e6028 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
@@ -120,12 +120,12 @@
       assert.equal(element._computeLabelValue(
           {labels: {Verified: {rejected: true}}}, 'Verified'), '✕');
 
-      assert.equal(element._computeProjectURL('combustible-stuff'),
-          '/q/status:open+project:combustible-stuff');
+      assert.equal(element._computeProjectURL('combustible/stuff'),
+          '/q/status:open+project:combustible%252Fstuff');
 
       assert.equal(element._computeProjectBranchURL(
-          'combustible-stuff', 'lemons'),
-          '/q/status:open+project:combustible-stuff+branch:lemons');
+          'combustible-stuff', 'le/mons'),
+          '/q/status:open+project:combustible-stuff+branch:le%252Fmons');
 
       element.change = {_number: 42};
       assert.equal(element.changeURL, '/c/42/');
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 b741784..09d5b84 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
@@ -32,19 +32,14 @@
   <template>
     <style>
       :host {
-        display: block;
+        display: inline-block;
+        font-family: var(--font-family);
       }
       section {
-        margin-top: 1em;
-      }
-      .groupLabel {
-        color: #666;
-        margin-bottom: .15em;
-        text-align: center;
+        display: inline-block;
       }
       gr-button {
-        display: block;
-        margin-bottom: .5em;
+        margin-left: .5em;
       }
       gr-button:before {
         content: attr(data-label);
@@ -53,6 +48,15 @@
         content: attr(data-loading-label);
       }
       @media screen and (max-width: 50em) {
+        :host,
+        section,
+        gr-button {
+          display: block;
+        }
+        gr-button {
+          margin-bottom: .5em;
+          margin-left: 0;
+        }
         .confirmDialog {
           width: 90vw;
         }
@@ -60,7 +64,6 @@
     </style>
     <div>
       <section hidden$="[[!_actionCount(actions.*, _additionalActions.*)]]">
-        <div class="groupLabel">Change</div>
         <template is="dom-repeat" items="[[_changeActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
               primary$="[[action.__primary]]"
@@ -73,7 +76,6 @@
         </template>
       </section>
       <section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]">
-        <div class="groupLabel">Revision</div>
         <template is="dom-repeat" items="[[_revisionActionValues]]" as="action">
           <gr-button title$="[[action.title]]"
               primary$="[[action.__primary]]"
@@ -94,13 +96,12 @@
           hidden></gr-confirm-rebase-dialog>
       <gr-confirm-cherrypick-dialog id="confirmCherrypick"
           class="confirmDialog"
-          commit-info="[[commitInfo]]"
+          message="[[commitMessage]]"
           on-confirm="_handleCherrypickConfirm"
           on-cancel="_handleConfirmDialogCancel"
           hidden></gr-confirm-cherrypick-dialog>
       <gr-confirm-revert-dialog id="confirmRevertDialog"
           class="confirmDialog"
-          commit-info="[[commitInfo]]"
           on-confirm="_handleRevertDialogConfirm"
           on-cancel="_handleConfirmDialogCancel"
           hidden></gr-confirm-revert-dialog>
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 3445f4e..8deab6b 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
@@ -75,7 +75,10 @@
       },
       changeNum: String,
       patchNum: String,
-      commitInfo: Object,
+      commitMessage: {
+        type: String,
+        value: '',
+      },
 
       _loading: {
         type: Boolean,
@@ -274,7 +277,7 @@
       if (type === ActionType.REVISION) {
         this._handleRevisionAction(key);
       } else if (key === ChangeActions.REVERT) {
-        this.$.confirmRevertDialog.populateRevertMessage();
+        this.$.confirmRevertDialog.populateRevertMessage(this.commitMessage);
         this.$.confirmRevertDialog.message = this._modifyRevertMsg();
         this._showActionDialog(this.$.confirmRevertDialog);
       } else if (key === ChangeActions.ABANDON) {
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 8b51312..1782061 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
@@ -128,18 +128,6 @@
       <span class="value">[[change.branch]]</span>
     </section>
     <section>
-      <span class="title">Commit</span>
-      <span class="value">
-        <template is="dom-if" if="[[_showWebLink]]">
-          <a target="_blank"
-             href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
-        </template>
-        <template is="dom-if" if="[[!_showWebLink]]">
-          [[_computeShortHash(commitInfo)]]
-        </template>
-      </span>
-    </section>
-    <section>
       <span class="title">Topic</span>
       <span class="value">
         <gr-editable-label
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 af19703..20c117e 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
@@ -27,17 +27,8 @@
 
     properties: {
       change: Object,
-      commitInfo: Object,
       mutable: Boolean,
       serverConfig: Object,
-      _showWebLink: {
-        type: Boolean,
-        computed: '_computeShowWebLink(change, commitInfo, serverConfig)',
-      },
-      _webLink: {
-        type: String,
-        computed: '_computeWebLink(change, commitInfo, serverConfig)',
-      },
       _topicReadOnly: {
         type: Boolean,
         computed: '_computeTopicReadOnly(mutable, change)',
@@ -52,38 +43,6 @@
       Gerrit.RESTClientBehavior,
     ],
 
-    _computeShowWebLink: function(change, commitInfo, serverConfig) {
-      var webLink = commitInfo.web_links && commitInfo.web_links.length;
-      var gitWeb = serverConfig.gitweb && serverConfig.gitweb.url &&
-          serverConfig.gitweb.type && serverConfig.gitweb.type.revision;
-      return webLink || gitWeb;
-    },
-
-    _computeWebLink: function(change, commitInfo, serverConfig) {
-      if (!this._computeShowWebLink(change, commitInfo, serverConfig)) {
-        return;
-      }
-
-      if (serverConfig.gitweb && serverConfig.gitweb.url &&
-          serverConfig.gitweb.type && serverConfig.gitweb.type.revision) {
-        return serverConfig.gitweb.url +
-            serverConfig.gitweb.type.revision
-                .replace('${project}', change.project)
-                .replace('${commit}', commitInfo.commit);
-      }
-
-      var webLink = commitInfo.web_links[0].url;
-      if (!/^https?\:\/\//.test(webLink)) {
-        webLink = '../../' + webLink;
-      }
-
-      return webLink;
-    },
-
-    _computeShortHash: function(commitInfo) {
-      return commitInfo.commit.slice(0, 7);
-    },
-
     _computeHideStrategy: function(change) {
       return !this.changeIsOpen(change.status);
     },
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 01f0649..a2d4946 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
@@ -20,11 +20,9 @@
 
 <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>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-change-metadata.html">
-<script src="../../../scripts/util.js"></script>
 
 <test-fixture id="basic">
   <template>
@@ -68,79 +66,6 @@
       assert.isTrue(element.$$('.strategy').hasAttribute('hidden'));
     });
 
-    test('no web link when unavailable', function() {
-      element.commitInfo = {};
-      element.serverConfig = {};
-      element.change = {labels: []};
-
-      assert.isNotOk(element._computeShowWebLink(element.change,
-          element.commitInfo, element.serverConfig));
-    });
-
-    test('use web link when available', function() {
-      element.commitInfo = {web_links: [{url: 'link-url'}]};
-      element.serverConfig = {};
-
-      assert.isOk(element._computeShowWebLink(element.change,
-          element.commitInfo, element.serverConfig));
-      assert.equal(element._computeWebLink(element.change, element.commitInfo,
-          element.serverConfig), '../../link-url');
-    });
-
-    test('does not relativize web links that begin with scheme', function() {
-      element.commitInfo = {web_links: [{url: 'https://link-url'}]};
-      element.serverConfig = {};
-
-      assert.isOk(element._computeShowWebLink(element.change,
-          element.commitInfo, element.serverConfig));
-      assert.equal(element._computeWebLink(element.change, element.commitInfo,
-          element.serverConfig), 'https://link-url');
-    });
-
-    test('use gitweb when available', function() {
-      element.commitInfo = {commit: 'commit-sha'};
-      element.serverConfig = {gitweb: {
-        url: 'url-base/',
-        type: {revision: 'xx ${project} xx ${commit} xx'},
-      }};
-      element.change = {
-        project: 'project-name',
-        labels: [],
-        current_revision: element.commitInfo.commit
-      };
-
-      assert.isOk(element._computeShowWebLink(element.change,
-          element.commitInfo, element.serverConfig));
-
-      assert.equal(element._computeWebLink(element.change, element.commitInfo,
-          element.serverConfig), 'url-base/xx project-name xx commit-sha xx');
-    });
-
-    test('prefer gitweb when both are available', function() {
-      element.commitInfo = {
-        commit: 'commit-sha',
-        web_links: [{url: 'link-url'}]
-      };
-      element.serverConfig = {gitweb: {
-        url: 'url-base/',
-        type: {revision: 'xx ${project} xx ${commit} xx'},
-      }};
-      element.change = {
-        project: 'project-name',
-        labels: [],
-        current_revision: element.commitInfo.commit
-      };
-
-      assert.isOk(element._computeShowWebLink(element.change,
-          element.commitInfo, element.serverConfig));
-
-      var link = element._computeWebLink(element.change, element.commitInfo,
-          element.serverConfig);
-
-      assert.equal(link, 'url-base/xx project-name xx commit-sha xx');
-      assert.notEqual(link, '../../link-url');
-    });
-
     test('show CC section when NoteDb enabled', function() {
       function hasCc() {
         return element._showReviewersByState;
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 e3f7fd2..54f07a0 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
@@ -29,6 +29,7 @@
 
 <link rel="import" href="../gr-change-actions/gr-change-actions.html">
 <link rel="import" href="../gr-change-metadata/gr-change-metadata.html">
+<link rel="import" href="../gr-commit-info/gr-commit-info.html">
 <link rel="import" href="../gr-download-dialog/gr-download-dialog.html">
 <link rel="import" href="../gr-file-list/gr-file-list.html">
 <link rel="import" href="../gr-messages-list/gr-messages-list.html">
@@ -45,18 +46,16 @@
         color: #666;
         padding: 1em var(--default-horizontal-margin);
       }
-      .headerContainer {
-        height: 4.1em;
-        margin-bottom: .5em;
-      }
       .header {
         align-items: center;
         background-color: var(--view-background-color);
-        border-bottom: 1px solid #ddd;
         display: flex;
-        padding: 1em var(--default-horizontal-margin);
+        padding: .65em var(--default-horizontal-margin);
         z-index: 99;  /* Less than gr-overlay's backdrop */
       }
+      .header .download {
+        margin-right: 1em;
+      }
       .header.pinned {
         border-bottom-color: transparent;
         box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
@@ -69,24 +68,11 @@
         flex: 1;
         font-size: 1.2em;
         font-weight: bold;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        white-space: nowrap;
       }
       gr-change-star {
         margin-right: .25em;
         vertical-align: -.425em;
       }
-      .download,
-      .patchSelectLabel {
-        margin-left: 1em;
-      }
-      .header select {
-        margin-left: .5em;
-      }
-      .header .reply {
-        margin-left: var(--default-horizontal-margin);
-      }
       gr-reply-dialog {
         width: 50em;
       }
@@ -94,45 +80,46 @@
         color: #999;
         text-transform: capitalize;
       }
-      section {
-        margin: 10px 0;
-        padding: 10px var(--default-horizontal-margin);
-      }
       /* Strong specificity here is needed due to
          https://github.com/Polymer/polymer/issues/2531 */
       .container section.changeInfo {
-        border-bottom: 1px solid #ddd;
         display: flex;
-        margin-top: 0;
-        padding-top: 0;
+        padding: 0 var(--default-horizontal-margin);
       }
       .changeInfo-column:not(:last-of-type) {
         margin-right: 1em;
         padding-right: 1em;
       }
       .changeMetadata {
-        border-right: 1px solid #ddd;
-        font-size: .9em;
-      }
-      gr-change-actions {
-        margin-top: 1em;
+        font-size: .95em;
       }
       .commitMessage {
         font-family: var(--monospace-font-family);
         flex: 0 0 72ch;
         margin-right: 2em;
         margin-bottom: 1em;
-        overflow-x: hidden;
-      }
-      .commitMessage h4 {
-        font-family: var(--font-family);
-        font-weight: bold;
-        margin-bottom: .25em;
       }
       .commitMessage gr-linked-text {
-        --linked-text-white-space: pre;
         overflow: auto;
       }
+      .editCommitMessage {
+        margin-top: 1em;
+      }
+      .commitActions {
+        border-bottom: 1px solid #ddd;
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: .5em;
+        padding-bottom: .5em;
+      }
+      .reply {
+        margin-right: .5em;
+      }
+      .mainChangeInfo {
+        display: flex;
+        flex: 1;
+        flex-direction: column;
+      }
       .commitAndRelated {
         align-content: flex-start;
         display: flex;
@@ -144,14 +131,30 @@
         font-size: .9em;
         overflow: hidden;
       }
+      .patchInfo {
+        border: 1px solid #ddd;
+        margin: 1em var(--default-horizontal-margin);
+      }
+      .patchInfo--oldPatchSet .patchInfo-header {
+        background-color: #fff9c4;
+      }
+      .patchInfo--oldPatchSet .latestPatchContainer {
+        display: initial;
+      }
+      .patchInfo-header,
       gr-file-list {
-        margin-bottom: 1em;
-        padding: 0 var(--default-horizontal-margin);
+        padding: .5em calc(var(--default-horizontal-margin) / 2);
+      }
+      .patchInfo-header {
+        background-color: #f6f6f6;
+        border-bottom: 1px solid #ebebeb;
+        display: flex;
+        justify-content: space-between;
+      }
+      .latestPatchContainer {
+        display: none;
       }
       @media screen and (max-width: 50em) {
-        .headerContainer {
-          height: 5.15em;
-        }
         .header {
           align-items: flex-start;
           flex-direction: column;
@@ -163,30 +166,17 @@
         .header-title {
           font-size: 1.1em;
         }
-        .header-actions {
-          align-items: center;
-          display: flex;
-          justify-content: space-between;
-          margin-top: .5em;
-        }
         gr-reply-dialog {
           min-width: initial;
           width: 90vw;
         }
-        .download {
+        .downloadContainer {
           display: none;
         }
-        .patchSelectLabel {
-          margin-left: 0;
-          margin-right: .5em;
-        }
-        .header select {
-          margin-left: 0;
-          margin-right: .5em;
-        }
-        .header .reply {
-          margin-left: 0;
-          margin-right: .5em;
+        .reply {
+          display: block;
+          margin-right: 0;
+          margin-bottom: .5em;
         }
         .changeInfo-column:not(:last-of-type) {
           margin-right: 0;
@@ -207,6 +197,9 @@
           margin-top: .25em;
           max-width: none;
         }
+        .commitActions {
+          flex-direction: column;
+        }
         .commitMessage {
           flex: initial;
           margin-right: 0;
@@ -215,85 +208,98 @@
     </style>
     <div class="container loading" hidden$="{{!_loading}}">Loading...</div>
     <div class="container" hidden$="{{_loading}}">
-      <div class="headerContainer">
-        <div class="header">
-          <span class="header-title">
-            <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
-            <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
-            <span>[[_change.subject]]</span>
-            <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span>
-          </span>
-          <span class="header-actions">
-            <gr-button hidden
-                class="reply"
-                primary$="[[_computeReplyButtonHighlighted(_diffDrafts.*)]]"
-                hidden$="[[!_loggedIn]]"
-                on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
-            <gr-button class="download" on-tap="_handleDownloadTap">Download</gr-button>
-            <span>
-              <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
-              <select id="patchSetSelect" on-change="_handlePatchChange">
-                <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
-                  <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
-                    <span>[[patchNumber]]</span>
-                    /
-                    <span>[[_computeLatestPatchNum(_allPatchSets)]]</span>
-                  </option>
-                </template>
-              </select>
-            </span>
-          </span>
-        </div>
+      <div class="header">
+        <span class="header-title">
+          <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
+          <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span>
+          <span>[[_change.subject]]</span>
+          <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span>
+        </span>
       </div>
       <section class="changeInfo">
         <div class="changeInfo-column changeMetadata">
           <gr-change-metadata
               change="{{_change}}"
-              commit-info="[[_commitInfo]]"
               server-config="[[serverConfig]]"
               mutable="[[_loggedIn]]"
               on-show-reply-dialog="_handleShowReplyDialog">
           </gr-change-metadata>
-          <gr-change-actions id="actions"
-              change="[[_change]]"
-              actions="[[_change.actions]]"
-              change-num="[[_changeNum]]"
-              patch-num="[[_patchRange.patchNum]]"
-              commit-info="[[_commitInfo]]"
-              on-reload-change="_handleReloadChange"></gr-change-actions>
         </div>
-        <div class="changeInfo-column commitAndRelated">
-          <div class="commitMessage">
-            <h4>
-              Commit message
+        <div class="changeInfo-column mainChangeInfo">
+          <div class="commitActions" hidden$="[[!_loggedIn]]"">
+            <gr-button
+                class="reply"
+                secondary
+                on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
+            <gr-change-actions id="actions"
+                change="[[_change]]"
+                actions="[[_change.actions]]"
+                change-num="[[_changeNum]]"
+                patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
+                commit-message="[[_latestCommitMessage]]"
+                on-reload-change="_handleReloadChange"></gr-change-actions>
+          </div>
+          <div class="commitAndRelated">
+            <div class="commitMessage">
+              <gr-editable-content id="commitMessageEditor"
+                  editing="[[_editingCommitMessage]]"
+                  content="{{_latestCommitMessage}}">
+                <gr-linked-text pre
+                    content="[[_latestCommitMessage]]"
+                    config="[[_projectConfig.commentlinks]]"></gr-linked-text>
+              </gr-editable-content>
               <gr-button link
+                  class="editCommitMessage"
                   on-tap="_handleEditCommitMessage"
                   hidden$="[[_hideEditCommitMessage]]">Edit</gr-button>
-            </h4>
-            <gr-editable-content id="commitMessageEditor"
-                editing="[[_editingCommitMessage]]"
-                content="{{_commitInfo.message}}">
-              <gr-linked-text pre
-                  content="[[_commitInfo.message]]"
-                  config="[[_projectConfig.commentlinks]]"></gr-linked-text>
-            </gr-editable-content>
-          </div>
-          <div class="relatedChanges">
-            <gr-related-changes-list id="relatedChanges"
-                change="[[_change]]"
-                patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list>
+            </div>
+            <div class="relatedChanges">
+              <gr-related-changes-list id="relatedChanges"
+                  change="[[_change]]"
+                  patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list>
+            </div>
           </div>
         </div>
       </section>
-      <gr-file-list id="fileList"
-          change="[[_change]]"
-          change-num="[[_changeNum]]"
-          patch-range="[[_patchRange]]"
-          comments="[[_comments]]"
-          drafts="[[_diffDrafts]]"
-          revisions="[[_change.revisions]]"
-          projectConfig="[[_projectConfig]]"
-          selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
+      <section class$="patchInfo [[_computePatchInfoClass(_patchRange.patchNum, _allPatchSets)]]">
+        <div class="patchInfo-header">
+          <div>
+            <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
+            <select id="patchSetSelect" on-change="_handlePatchChange">
+              <template is="dom-repeat" items="[[_allPatchSets]]" as="patchNumber">
+                <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]">
+                  <span>[[patchNumber]]</span>
+                  /
+                  <span>[[_computeLatestPatchNum(_allPatchSets)]]</span>
+                </option>
+              </template>
+            </select>
+            <span class="downloadContainer">
+              /
+              <gr-button link
+                  class="download"
+                  on-tap="_handleDownloadTap">Download</gr-button>
+            </span>
+            <span class="latestPatchContainer">
+              /
+              <a href$="/c/[[_change._number]]">Go to latest patch set</a>
+            </span>
+          </div>
+          <gr-commit-info
+              change="[[_change]]"
+              server-config="[[serverConfig]]"
+              commit-info="[[_commitInfo]]"></gr-commit-info>
+        </div>
+        <gr-file-list id="fileList"
+            change="[[_change]]"
+            change-num="[[_changeNum]]"
+            patch-range="[[_patchRange]]"
+            comments="[[_comments]]"
+            drafts="[[_diffDrafts]]"
+            revisions="[[_change.revisions]]"
+            projectConfig="[[_projectConfig]]"
+            selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
+      </section>
       <gr-messages-list id="messageList"
           change-num="[[_changeNum]]"
           messages="[[_change.messages]]"
@@ -312,12 +318,12 @@
           on-close="_handleDownloadDialogClose"></gr-download-dialog>
     </gr-overlay>
     <gr-overlay id="replyOverlay"
+        no-cancel-on-outside-click
         on-iron-overlay-opened="_handleReplyOverlayOpen"
         with-backdrop>
       <gr-reply-dialog id="replyDialog"
           change="[[_change]]"
-          patch-num="[[_patchRange.patchNum]]"
-          revisions="[[_change.revisions]]"
+          patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"
           labels="[[_change.labels]]"
           permitted-labels="[[_change.permitted_labels]]"
           diff-drafts="[[_diffDrafts]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 14ac4d1..04c2c04 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -66,7 +66,11 @@
       _hideEditCommitMessage: {
         type: Boolean,
         computed: '_computeHideEditCommitMessage(_loggedIn, ' +
-            '_editingCommitMessage, _change.*, _patchRange.patchNum)',
+            '_editingCommitMessage, _change)',
+      },
+      _latestCommitMessage: {
+        type: String,
+        value: '',
       },
       _patchRange: Object,
       _allPatchSets: {
@@ -78,8 +82,6 @@
         value: false,
       },
       _loading: Boolean,
-      _headerContainerEl: Object,
-      _headerEl: Object,
       _projectConfig: Object,
       _replyButtonLabel: {
         type: String,
@@ -98,10 +100,6 @@
       '_paramsAndChangeChanged(params, _change)',
     ],
 
-    ready: function() {
-      this._headerEl = this.$$('.header');
-    },
-
     attached: function() {
       this._getLoggedIn().then(function(loggedIn) {
         this._loggedIn = loggedIn;
@@ -114,34 +112,6 @@
           this._handleCommitMessageSave.bind(this));
       this.addEventListener('editable-content-cancel',
           this._handleCommitMessageCancel.bind(this));
-      this.listen(window, 'scroll', '_handleBodyScroll');
-    },
-
-    detached: function() {
-      this.unlisten(window, 'scroll', '_handleBodyScroll');
-    },
-
-    _handleBodyScroll: function(e) {
-      var containerEl = this._headerContainerEl ||
-          this.$$('.headerContainer');
-
-      // Calculate where the header is relative to the window.
-      var top = containerEl.offsetTop;
-      for (var offsetParent = containerEl.offsetParent;
-           offsetParent;
-           offsetParent = offsetParent.offsetParent) {
-        top += offsetParent.offsetTop;
-      }
-      // The element may not be displayed yet, in which case do nothing.
-      if (top == 0) { return; }
-
-      this._headerEl.classList.toggle('pinned', window.scrollY >= top);
-    },
-
-    _resetHeaderEl: function() {
-      var el = this._headerEl || this.$$('.header');
-      this._headerEl = el;
-      el.classList.remove('pinned');
     },
 
     _handleEditCommitMessage: function(e) {
@@ -157,7 +127,7 @@
         this.$.commitMessageEditor.disabled = false;
         if (!resp.ok) { return; }
 
-        this.set('_commitInfo.message', message);
+        this._latestCommitMessage = message;
         this._editingCommitMessage = false;
         this._reloadWindow();
       }.bind(this)).catch(function(err) {
@@ -182,16 +152,8 @@
           }.bind(this));
     },
 
-    _computeHideEditCommitMessage: function(loggedIn, editing, changeRecord,
-        patchNum) {
-      if (!changeRecord || !loggedIn || editing) { return true; }
-
-      patchNum = parseInt(patchNum, 10);
-      if (isNaN(patchNum)) { return true; }
-
-      var change = changeRecord.base;
-      if (!change.current_revision) { return true; }
-      if (change.revisions[change.current_revision]._number !== patchNum) {
+    _computeHideEditCommitMessage: function(loggedIn, editing, change) {
+      if (!loggedIn || editing || change.status === this.ChangeStatus.MERGED) {
         return true;
       }
 
@@ -267,19 +229,7 @@
     },
 
     _handlePatchChange: function(e) {
-      var patchNum = e.target.value;
-      var currentPatchNum;
-      if (this._change.current_revision) {
-        currentPatchNum =
-            this._change.revisions[this._change.current_revision]._number;
-      } else {
-        currentPatchNum = this._computeLatestPatchNum(this._allPatchSets);
-      }
-      if (patchNum == currentPatchNum) {
-        page.show(this.changePath(this._changeNum));
-        return;
-      }
-      page.show(this.changePath(this._changeNum) + '/' + patchNum);
+      this._changePatchNum(parseInt(e.target.value, 10));
     },
 
     _handleReplyTap: function(e) {
@@ -339,9 +289,6 @@
       };
 
       this._reload().then(function() {
-        this.$.messageList.topMargin = this._headerEl.offsetHeight;
-        this.$.fileList.topMargin = this._headerEl.offsetHeight;
-
         // Allow the message list to render before scrolling.
         this.async(function() {
           this._maybeScrollToMessage();
@@ -389,6 +336,12 @@
 
     _resetFileListViewState: function() {
       this.set('viewState.selectedFileIndex', 0);
+      if (!!this.viewState.changeNum &&
+          this.viewState.changeNum !== this._changeNum) {
+        // Reset the diff mode to null when navigating from one change to
+        // another, so that the user's preference is restored.
+        this.set('viewState.diffMode', null);
+      }
       this.set('viewState.changeNum', this._changeNum);
       this.set('viewState.patchRange', this._patchRange);
     },
@@ -405,6 +358,25 @@
       this.fire('title-change', {title: title});
     },
 
+    /**
+     * Change active patch to the provided patch num.
+     * @param {int} patchNum the patchn number to be viewed.
+     */
+    _changePatchNum: function(patchNum) {
+      var currentPatchNum;
+      if (this._change.current_revision) {
+        currentPatchNum =
+            this._change.revisions[this._change.current_revision]._number;
+      } else {
+        currentPatchNum = this._computeLatestPatchNum(this._allPatchSets);
+      }
+      if (patchNum === currentPatchNum) {
+        page.show(this.changePath(this._changeNum));
+        return;
+      }
+      page.show(this.changePath(this._changeNum) + '/' + patchNum);
+    },
+
     _computeChangePermalink: function(changeNum) {
       return '/' + changeNum;
     },
@@ -426,6 +398,14 @@
       return allPatchSets[allPatchSets.length - 1];
     },
 
+    _computePatchInfoClass: function(patchNum, allPatchSets) {
+      if (parseInt(patchNum, 10) ===
+          this._computeLatestPatchNum(allPatchSets)) {
+        return '';
+      }
+      return 'patchInfo--oldPatchSet';
+    },
+
     _computeAllPatchSets: function(change) {
       var patchNums = [];
       for (var rev in change.revisions) {
@@ -477,11 +457,6 @@
       return result;
     },
 
-    _computeReplyButtonHighlighted: function(changeRecord) {
-      var drafts = (changeRecord && changeRecord.base) || {};
-      return Object.keys(drafts).length > 0;
-    },
-
     _computeReplyButtonLabel: function(changeRecord) {
       var drafts = (changeRecord && changeRecord.base) || {};
       var draftCount = Object.keys(drafts).reduce(function(count, file) {
@@ -495,9 +470,17 @@
       return label;
     },
 
+    _switchToMostRecentPatchNum: function() {
+      this._getChangeDetail().then(function() {
+        var patchNum = this._allPatchSets[this._allPatchSets.length - 1];
+        if (patchNum !== this._patchRange.patchNum) {
+          this._changePatchNum(patchNum);
+        }
+      }.bind(this));
+    },
+
     _handleKey: function(e) {
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
-
       switch (e.keyCode) {
         case 65:  // 'a'
           if (this._loggedIn && !e.shiftKey) {
@@ -505,6 +488,16 @@
             this._openReplyDialog();
           }
           break;
+        case 68: // 'd'
+          e.preventDefault();
+          this.$.downloadOverlay.open();
+          break;
+        case 82: // 'r'
+          if (e.shiftKey) {
+            e.preventDefault();
+            this._switchToMostRecentPatchNum();
+          }
+          break;
         case 85:  // 'u'
           e.preventDefault();
           page.show('/');
@@ -572,6 +565,14 @@
           }.bind(this));
     },
 
+    _getLatestCommitMessage: function() {
+      return this.$.restAPI.getChangeCommitInfo(this._changeNum,
+          this._computeLatestPatchNum(this._allPatchSets)).then(
+              function(commitInfo) {
+                this._latestCommitMessage = commitInfo.message;
+              }.bind(this));
+    },
+
     _getCommitInfo: function() {
       return this.$.restAPI.getChangeCommitInfo(
           this._changeNum, this._patchRange.patchNum).then(
@@ -606,7 +607,6 @@
       var reloadPatchNumDependentResources = function() {
         return Promise.all([
           this._getCommitInfo(),
-          this.$.actions.reload(),
           this.$.fileList.reload(),
         ]);
       }.bind(this);
@@ -614,13 +614,13 @@
         if (!this._change) { return Promise.resolve(); }
 
         return Promise.all([
+          this._getLatestCommitMessage(),
+          this.$.actions.reload(),
           this.$.relatedChanges.reload(),
           this._getProjectConfig(),
         ]);
       }.bind(this);
 
-      this._resetHeaderEl();
-
       if (this._patchRange.patchNum) {
         return reloadPatchNumDependentResources().then(function() {
           return detailCompletes;
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 c9a687b..1c36958 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
@@ -43,44 +43,96 @@
       element = fixture('basic');
     });
 
-    test('keyboard shortcuts', function() {
-      var showStub = sinon.stub(page, 'show');
+    suite('keyboard shortcuts', function() {
+      test('U should navigate to /', function() {
+        var showStub = sinon.stub(page, 'show');
+        MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'U'
+        assert(showStub.lastCall.calledWithExactly('/'));
+        showStub.restore();
+      });
 
-      MockInteractions.pressAndReleaseKeyOn(element, 85);  // 'U'
-      assert(showStub.lastCall.calledWithExactly('/'),
-          'Should navigate to /');
-      showStub.restore();
+      test('A should toggle overlay', function() {
+        MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
+        var overlayEl = element.$.replyOverlay;
+        assert.isFalse(overlayEl.opened);
+        element._loggedIn = true;
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
-      var overlayEl = element.$.replyOverlay;
-      assert.isFalse(overlayEl.opened);
-      element._loggedIn = true;
+        MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
+        assert.isFalse(overlayEl.opened);
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
-      assert.isFalse(overlayEl.opened);
+        MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
+        assert.isTrue(overlayEl.opened);
+        overlayEl.close();
+        assert.isFalse(overlayEl.opened);
+      });
 
-      MockInteractions.pressAndReleaseKeyOn(element, 65);  // 'A'
-      assert.isTrue(overlayEl.opened);
-      overlayEl.close();
-      assert.isFalse(overlayEl.opened);
+      test('shift + R should fetch and navigate to the latest patch set',
+          function(done) {
+        element._changeNum = '42';
+        element._patchRange = {
+          basePatchNum: 'PARENT',
+          patchNum: 1,
+        };
+        element._change = {
+          change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+          revisions: {
+            rev1: {_number: 1},
+          },
+          current_revision: 'rev1',
+          status: 'NEW',
+          labels: {},
+          actions: {},
+        };
+
+        sinon.stub(element.$.restAPI, '_getChangeDetail', function() {
+          // Mock change obj.
+          return Promise.resolve({
+            change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+            revisions: {
+              rev1: {_number: 1},
+              rev13: {_number: 13},
+            },
+            current_revision: 'rev1',
+            status: 'NEW',
+            labels: {},
+            actions: {},
+          });
+        });
+
+        var showStub = sinon.stub(page, 'show', function(arg) {
+          assert.equal(arg, '/c/42/13');
+          showStub.restore();
+          element.$.restAPI._getChangeDetail.restore();
+          done();
+        });
+
+        // 'shift + R'
+        MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift');
+      });
+
+      test('d should open download overlay', function() {
+        var stub = sinon.stub(element.$.downloadOverlay, 'open');
+        MockInteractions.pressAndReleaseKeyOn(element, 68); // 'd'
+        assert.isTrue(stub.called);
+        stub.restore();
+      });
     });
 
-    test('reply button is highlighted when there are drafts', function() {
+    test('reply button has updated count when there are drafts', function() {
       var replyButton = element.$$('gr-button.reply');
       assert.ok(replyButton);
-      assert.isFalse(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply');
 
       element._diffDrafts = null;
-      assert.isFalse(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply');
 
       element._diffDrafts = {};
-      assert.isFalse(replyButton.hasAttribute('primary'));
+      assert.equal(replyButton.textContent, 'Reply');
 
       element._diffDrafts = {
         'file1.txt': [{}],
         'file2.txt': [{}, {}],
       };
-      assert.isTrue(replyButton.hasAttribute('primary'));
       assert.equal(replyButton.textContent, 'Reply (3)');
     });
 
@@ -120,6 +172,36 @@
       assert.deepEqual(element._diffDrafts, {});
     });
 
+    test('change num change', function() {
+      element._changeNum = null;
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 2,
+      };
+      element._change = {
+        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+        labels: {},
+      };
+      element.viewState.changeNum = null;
+      element.viewState.diffMode = 'UNIFIED';
+      flushAsynchronousOperations();
+      assert.equal(element.viewState.diffMode, 'UNIFIED');
+
+      element._changeNum = '1';
+      element.params = {changeNum: '1'};
+      element._change.newProp = '1';
+      flushAsynchronousOperations();
+      assert.equal(element.viewState.diffMode, 'UNIFIED');
+      assert.equal(element.viewState.changeNum, '1');
+
+      element._changeNum = '2';
+      element.params = {changeNum: '2'};
+      element._change.newProp = '2';
+      flushAsynchronousOperations();
+      assert.isNull(element.viewState.diffMode);
+      assert.equal(element.viewState.changeNum, '2');
+    });
+
     test('patch num change', function(done) {
       element._changeNum = '42';
       element._patchRange = {
@@ -138,24 +220,27 @@
         status: 'NEW',
         labels: {},
       };
+      element.viewState.diffMode = 'UNIFIED';
       flushAsynchronousOperations();
-      var selectEl = element.$$('.header select');
+
+      var selectEl = element.$$('.patchInfo-header select');
       assert.ok(selectEl);
-      var optionEls =
-          Polymer.dom(element.root).querySelectorAll('.header option');
+      var optionEls = Polymer.dom(element.root).querySelectorAll(
+          '.patchInfo-header option');
       assert.equal(optionEls.length, 4);
-      assert.isFalse(
-          element.$$('.header option[value="1"]').hasAttribute('selected'));
-      assert.isTrue(
-          element.$$('.header option[value="2"]').hasAttribute('selected'));
-      assert.isFalse(
-          element.$$('.header option[value="3"]').hasAttribute('selected'));
+      assert.isFalse(element.$$('.patchInfo-header option[value="1"]')
+          .hasAttribute('selected'));
+      assert.isTrue(element.$$('.patchInfo-header option[value="2"]')
+          .hasAttribute('selected'));
+      assert.isFalse(element.$$('.patchInfo-header option[value="3"]')
+          .hasAttribute('selected'));
       assert.equal(optionEls[3].value, 13);
 
       var showStub = sinon.stub(page, 'show');
 
       var numEvents = 0;
       selectEl.addEventListener('change', function(e) {
+        assert.equal(element.viewState.diffMode, 'UNIFIED');
         numEvents++;
         if (numEvents == 1) {
           assert(showStub.lastCall.calledWithExactly('/c/42/1'),
@@ -191,17 +276,17 @@
         labels: {},
       };
       flushAsynchronousOperations();
-      var selectEl = element.$$('.header select');
+      var selectEl = element.$$('.patchInfo-header select');
       assert.ok(selectEl);
-      var optionEls =
-          Polymer.dom(element.root).querySelectorAll('.header option');
+      var optionEls = Polymer.dom(element.root).querySelectorAll(
+          '.patchInfo-header option');
       assert.equal(optionEls.length, 4);
-      assert.isFalse(
-          element.$$('.header option[value="1"]').hasAttribute('selected'));
-      assert.isTrue(
-          element.$$('.header option[value="2"]').hasAttribute('selected'));
-      assert.isFalse(
-          element.$$('.header option[value="3"]').hasAttribute('selected'));
+      assert.isFalse(element.$$('.patchInfo-header option[value="1"]')
+          .hasAttribute('selected'));
+      assert.isTrue(element.$$('.patchInfo-header option[value="2"]')
+          .hasAttribute('selected'));
+      assert.isFalse(element.$$('.patchInfo-header option[value="3"]')
+          .hasAttribute('selected'));
       assert.equal(optionEls[3].value, 13);
 
       var showStub = sinon.stub(page, 'show');
@@ -287,33 +372,26 @@
     });
 
     test('show commit message edit button', function() {
-      var changeRecord = {
-        base: {
-          revisions: {
-            rev1: {_number: 1},
-            rev2: {_number: 2},
-          },
-          current_revision: 'rev2',
-        },
+      var _change = {
+        status: element.ChangeStatus.MERGED,
       };
-      assert.isTrue(element._computeHideEditCommitMessage(
-          false, false, changeRecord, '2'));
-      assert.isTrue(element._computeHideEditCommitMessage(
-          true, true, changeRecord, '2'));
-      assert.isTrue(element._computeHideEditCommitMessage(
-          true, false, changeRecord, '1'));
-      assert.isFalse(element._computeHideEditCommitMessage(
-          true, false, changeRecord, '2'));
+      assert.isTrue(element._computeHideEditCommitMessage(false, false, {}));
+      assert.isTrue(element._computeHideEditCommitMessage(true, true, {}));
+      assert.isTrue(element._computeHideEditCommitMessage(false, true, {}));
+      assert.isFalse(element._computeHideEditCommitMessage(true, false, {}));
+      assert.isTrue(element._computeHideEditCommitMessage(true, false,
+          _change));
     });
 
-    test('topic is coalesced to null', function() {
+    test('topic is coalesced to null', function(done) {
       sinon.stub(element, '_changeChanged');
-      sinon.stub(element.$.restAPI, 'getChangeDetail', function(num) {
+      sinon.stub(element.$.restAPI, 'getChangeDetail', function() {
         return Promise.resolve({id: '123456789', labels: {}});
       });
 
       element._getChangeDetail().then(function() {
         assert.isNull(element._change.topic);
+        done();
       });
     });
 
@@ -331,5 +409,14 @@
       assert(openSpy.lastCall.calledWithExactly(FocusTarget.CCS),
           '_openReplyDialog should have been passed CCS');
     });
+
+    test('class is applied to file list on old patch set', function() {
+      var allPatcheSets = [1, 2, 4];
+      assert.equal(element._computePatchInfoClass('1', allPatcheSets),
+          'patchInfo--oldPatchSet');
+      assert.equal(element._computePatchInfoClass('2', allPatcheSets),
+          'patchInfo--oldPatchSet');
+      assert.equal(element._computePatchInfoClass('4', allPatcheSets), '');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
new file mode 100644
index 0000000..5cd65fa
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
@@ -0,0 +1,35 @@
+<!--
+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-commit-info">
+  <template>
+    <style>
+      :host {
+        display: inline-block;
+      }
+    </style>
+    <template is="dom-if" if="[[_showWebLink]]">
+      <a target="_blank"
+         href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a>
+    </template>
+    <template is="dom-if" if="[[!_showWebLink]]">
+      [[_computeShortHash(commitInfo)]]
+    </template>
+  </template>
+  <script src="gr-commit-info.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
new file mode 100644
index 0000000..5aa8601
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -0,0 +1,98 @@
+// 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-commit-info',
+
+    properties: {
+      change: Object,
+      commitInfo: Object,
+      serverConfig: Object,
+      _showWebLink: {
+        type: Boolean,
+        computed: '_computeShowWebLink(change, commitInfo, serverConfig)',
+      },
+      _webLink: {
+        type: String,
+        computed: '_computeWebLink(change, commitInfo, serverConfig)',
+      },
+    },
+
+    _isWebLink: function(link) {
+      // This is a whitelist of web link types that provide direct links to
+      // the commit in the url property.
+      return link.name === 'gitiles' || link.name === 'gitweb';
+    },
+
+    _computeShowWebLink: function(change, commitInfo, serverConfig) {
+      if (serverConfig.gitweb && serverConfig.gitweb.url &&
+          serverConfig.gitweb.type && serverConfig.gitweb.type.revision) {
+        return true;
+      }
+
+      if (!commitInfo.web_links) {
+        return false;
+      }
+
+      for (var i = 0; i < commitInfo.web_links.length; i++) {
+        if (this._isWebLink(commitInfo.web_links[i])) {
+          return true;
+        }
+      }
+
+      return false;
+    },
+
+    _computeWebLink: function(change, commitInfo, serverConfig) {
+      if (!this._computeShowWebLink(change, commitInfo, serverConfig)) {
+        return;
+      }
+
+      if (serverConfig.gitweb && serverConfig.gitweb.url &&
+          serverConfig.gitweb.type && serverConfig.gitweb.type.revision) {
+        return serverConfig.gitweb.url +
+            serverConfig.gitweb.type.revision
+                .replace('${project}', change.project)
+                .replace('${commit}', commitInfo.commit);
+      }
+
+      var webLink = null;
+      for (var i = 0; i < commitInfo.web_links.length; i++) {
+        if (this._isWebLink(commitInfo.web_links[i])) {
+          webLink = commitInfo.web_links[i].url;
+          break;
+        }
+      }
+
+      if (!webLink) {
+        return;
+      }
+
+      if (!/^https?\:\/\//.test(webLink)) {
+        webLink = '../../' + webLink;
+      }
+
+      return webLink;
+    },
+
+    _computeShortHash: function(commitInfo) {
+      if (!commitInfo || !commitInfo.commit) {
+        return;
+      }
+      return commitInfo.commit.slice(0, 7);
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
new file mode 100644
index 0000000..36b1628
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -0,0 +1,145 @@
+<!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-commit-info</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-commit-info.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-commit-info></gr-commit-info>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-commit-info tests', function() {
+    var element;
+
+    setup(function() {
+      element = fixture('basic');
+    });
+
+    test('no web link when unavailable', function() {
+      element.commitInfo = {};
+      element.serverConfig = {};
+      element.change = {labels: []};
+
+      assert.isNotOk(element._computeShowWebLink(element.change,
+          element.commitInfo, element.serverConfig));
+    });
+
+    test('use web link when available', function() {
+      element.commitInfo = {web_links: [{name: 'gitweb', url: 'link-url'}]};
+      element.serverConfig = {};
+
+      assert.isOk(element._computeShowWebLink(element.change,
+          element.commitInfo, element.serverConfig));
+      assert.equal(element._computeWebLink(element.change, element.commitInfo,
+          element.serverConfig), '../../link-url');
+    });
+
+    test('does not relativize web links that begin with scheme', function() {
+      element.commitInfo = {
+        web_links: [{name: 'gitweb', url: 'https://link-url'}]
+      };
+      element.serverConfig = {};
+
+      assert.isOk(element._computeShowWebLink(element.change,
+          element.commitInfo, element.serverConfig));
+      assert.equal(element._computeWebLink(element.change, element.commitInfo,
+          element.serverConfig), 'https://link-url');
+    });
+
+    test('use gitweb when available', function() {
+      element.commitInfo = {commit: 'commit-sha'};
+      element.serverConfig = {gitweb: {
+        url: 'url-base/',
+        type: {revision: 'xx ${project} xx ${commit} xx'},
+      }};
+      element.change = {
+        project: 'project-name',
+        labels: [],
+        current_revision: element.commitInfo.commit
+      };
+
+      assert.isOk(element._computeShowWebLink(element.change,
+          element.commitInfo, element.serverConfig));
+
+      assert.equal(element._computeWebLink(element.change, element.commitInfo,
+          element.serverConfig), 'url-base/xx project-name xx commit-sha xx');
+    });
+
+    test('prefer gitweb when both are available', function() {
+      element.commitInfo = {
+        commit: 'commit-sha',
+        web_links: [{url: 'link-url'}]
+      };
+      element.serverConfig = {gitweb: {
+        url: 'url-base/',
+        type: {revision: 'xx ${project} xx ${commit} xx'},
+      }};
+      element.change = {
+        project: 'project-name',
+        labels: [],
+        current_revision: element.commitInfo.commit
+      };
+
+      assert.isOk(element._computeShowWebLink(element.change,
+          element.commitInfo, element.serverConfig));
+
+      var link = element._computeWebLink(element.change, element.commitInfo,
+          element.serverConfig);
+
+      assert.equal(link, 'url-base/xx project-name xx commit-sha xx');
+      assert.notEqual(link, '../../link-url');
+    });
+
+    test('ignore web links that are neither gitweb nor gitiles', function() {
+      element.commitInfo = {
+        commit: 'commit-sha',
+        web_links: [
+          {
+            name: 'ignore',
+            url: 'ignore',
+          },
+          {
+            name: 'gitiles',
+            url: 'https://link-url',
+          }
+        ],
+      };
+      element.serverConfig = {};
+
+      assert.isOk(element._computeShowWebLink(element.change,
+          element.commitInfo, element.serverConfig));
+      assert.equal(element._computeWebLink(element.change, element.commitInfo,
+          element.serverConfig), 'https://link-url');
+
+      // Remove gitiles link.
+      element.commitInfo.web_links.splice(1, 1);
+      assert.isNotOk(element._computeShowWebLink(element.change,
+          element.commitInfo, element.serverConfig));
+      assert.isNotOk(element._computeWebLink(element.change, element.commitInfo,
+          element.serverConfig));
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
index 97342d1..f27e4e2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -32,16 +32,6 @@
     properties: {
       branch: String,
       message: String,
-      commitInfo: {
-        type: Object,
-        readOnly: true,
-        observer: '_commitInfoChanged',
-      },
-    },
-
-    _commitInfoChanged: function(commitInfo) {
-      // Pre-populate cherry-pick message for editing from commit info.
-      this.message = commitInfo.message;
     },
 
     _handleConfirmTap: function(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index b4baa26..2eb6646 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -30,26 +30,22 @@
      */
 
     properties: {
-      branch: String,
       message: String,
-      commitInfo: Object,
     },
 
-    populateRevertMessage: function() {
+    populateRevertMessage: function(message) {
       // Figure out what the revert title should be.
-      var originalTitle = this.commitInfo.message.split('\n')[0];
-      var revertTitle = 'Revert of ' + originalTitle;
-      if (originalTitle.startsWith('Revert of ')) {
-        revertTitle = 'Reland of ' +
-                      originalTitle.substring('Revert of '.length);
-      } else if (originalTitle.startsWith('Reland of ')) {
-        revertTitle = 'Revert of ' +
-                      originalTitle.substring('Reland of '.length);
-      }
+      var originalTitle = message.split('\n')[0];
+      var revertTitle = 'Revert "' + originalTitle + '"';
+      // Figure out what the revert commit message should be.
+      var commitRegex = /\n{1,2}\nChange-Id: (\w+)\n/gm;
+      var match = commitRegex.exec(message);
+      var revertCommitText = 'This reverts commit ' + match[1] + '.';
       // Add '> ' in front of the original commit text.
-      var originalCommitText = this.commitInfo.message.replace(/^/gm, '> ');
+      var originalCommitText = message.replace(/^/gm, '> ');
 
       this.message = revertTitle + '\n\n' +
+                     revertCommitText + '\n\n' +
                      'Reason for revert: <INSERT REASONING HERE>\n\n' +
                      'Original issue\'s description:\n' + originalCommitText;
     },
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
index 1d53eef..521aeef 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -40,25 +40,39 @@
 
     test('single line', function() {
       assert.isNotOk(element.message);
-      element.commitInfo = {message: 'one line commit'};
-      assert.isNotOk(element.message);
-      element.populateRevertMessage();
-      var expected = 'Revert of one line commit\n\n' +
+      element.populateRevertMessage('one line commit\n\nChange-Id: abcdefg\n');
+      var expected = 'Revert "one line commit"\n\n' +
+                     'This reverts commit abcdefg.\n\n' +
                      'Reason for revert: <INSERT REASONING HERE>\n\n' +
                      'Original issue\'s description:\n' +
-                     '> one line commit';
+                     '> one line commit\n> \n' +
+                     '> Change-Id: abcdefg\n> ';
       assert.equal(element.message, expected);
     });
 
     test('multi line', function() {
       assert.isNotOk(element.message);
-      element.commitInfo = {message: 'many lines\ncommit\n\nmessage\n'};
-      assert.isNotOk(element.message);
-      element.populateRevertMessage();
-      var expected = 'Revert of many lines\n\n' +
+      element.populateRevertMessage(
+          'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n');
+      var expected = 'Revert "many lines"\n\n' +
+                     'This reverts commit abcdefg.\n\n' +
                      'Reason for revert: <INSERT REASONING HERE>\n\n' +
                      'Original issue\'s description:\n' +
-                     '> many lines\n> commit\n> \n> message\n> ';
+                     '> many lines\n> commit\n> \n> message\n> \n' +
+                     '> Change-Id: abcdefg\n> ';
+      assert.equal(element.message, expected);
+    });
+
+    test('revert a revert', function () {
+      assert.isNotOk(element.message);
+      element.populateRevertMessage(
+          'Revert "one line commit"\n\nChange-Id: abcdefg\n');
+      var expected = 'Revert "Revert "one line commit""\n\n' +
+                     'This reverts commit abcdefg.\n\n' +
+                     'Reason for revert: <INSERT REASONING HERE>\n\n' +
+                     'Original issue\'s description:\n' +
+                     '> Revert "one line commit"\n> \n' +
+                     '> Change-Id: abcdefg\n> ';
       assert.equal(element.message, expected);
     });
   });
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index ef5ceed..a47c27c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -100,6 +100,20 @@
         color: #C62828;
         font-weight: bold;
       }
+      .show-hide {
+        margin-left: .4em;
+      }
+      input.show-hide {
+        display: none;
+      }
+      label.show-hide {
+        color: #00f;
+        cursor: pointer;
+        display: block;
+        font-size: .8em;
+        min-width: 2em;
+        margin-top: .1em;
+      }
       gr-diff {
         box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
         display: block;
@@ -146,7 +160,8 @@
       <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]">
         <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
           <input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]"
-              data-path$="[[file.__path]]" on-change="_handleReviewedChange">
+              data-path$="[[file.__path]]" on-change="_handleReviewedChange"
+              class="reviewed">
         </div>
         <div class$="[[_computeClass('status', file.__path)]]">
           [[_computeFileStatus(file.status)]]
@@ -168,8 +183,17 @@
           <span class="added">+[[file.lines_inserted]]</span>
           <span class="removed">-[[file.lines_deleted]]</span>
         </div>
+        <div class="show-hide">
+          <label class="show-hide">
+            <input type="checkbox" class="show-hide"
+                checked$="[[!file.__expanded]]" data-path$="[[file.__path]]"
+                on-change="_handleHiddenChange">
+            [[_computeShowHideText(file.__expanded)]]
+          </label>
+        </div>
       </div>
-      <gr-diff hidden
+      <gr-diff
+          hidden$="[[_computeHiddenState(file.__expanded)]]"
           project="[[change.project]]"
           commit="[[change.current_revision]]"
           change-num="[[changeNum]]"
@@ -181,9 +205,7 @@
     </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
-    <gr-diff-cursor
-        id="cursor"
-        fold-offset-top="[[topMargin]]"></gr-diff-cursor>
+    <gr-diff-cursor id="cursor"></gr-diff-cursor>
   </template>
   <script src="gr-file-list.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 225d8b3..eece232 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -27,7 +27,6 @@
       drafts: Object,
       revisions: Object,
       projectConfig: Object,
-      topMargin: Number,
       selectedIndex: {
         type: Number,
         notify: true,
@@ -121,6 +120,11 @@
       return parseInt(patchNum, 10) === parseInt(basePatchNum, 10);
     },
 
+    _handleHiddenChange: function(e) {
+      var model = e.model;
+      model.set('file.__expanded', !model.file.__expanded);
+    },
+
     _handlePatchChange: function(e) {
       this.set('patchRange.basePatchNum', Polymer.dom(e).rootTarget.value);
       page.show('/c/' + encodeURIComponent(this.changeNum) + '/' +
@@ -136,10 +140,9 @@
 
     _expandAllDiffs: function(e) {
       this._showInlineDiffs = true;
-      this._forEachDiff(function(diff) {
-        diff.hidden = false;
-        diff.reload();
-      });
+      for (var index in this._files) {
+        this.set(['_files', index, '__expanded'], true);
+      }
       if (e && e.target) {
         e.target.blur();
       }
@@ -147,9 +150,9 @@
 
     _collapseAllDiffs: function(e) {
       this._showInlineDiffs = false;
-      this._forEachDiff(function(diff) {
-        diff.hidden = true;
-      });
+      for (var index in this._files) {
+        this.set(['_files', index, '__expanded'], false);
+      }
       this.$.cursor.handleDiffUpdate();
       if (e && e.target) {
         e.target.blur();
@@ -212,12 +215,17 @@
 
     _getFiles: function() {
       return this.$.restAPI.getChangeFilesAsSpeciallySortedArray(
-          this.changeNum, this.patchRange);
+          this.changeNum, this.patchRange).then(function(files) {
+            // Append UI-specific properties.
+            return files.map(function(file) {
+              file.__expanded = false;
+              return file;
+            });
+          });
     },
 
     _handleKey: function(e) {
       if (this.shouldSupressKeyboardShortcut(e)) { return; }
-
       switch (e.keyCode) {
         case 37: // left
           if (e.shiftKey && this._showInlineDiffs) {
@@ -232,9 +240,14 @@
           }
           break;
         case 73:  // 'i'
-          if (!e.shiftKey) { return; }
-          e.preventDefault();
-          this._toggleInlineDiffs();
+          if (e.shiftKey) {
+            e.preventDefault();
+            this._toggleInlineDiffs();
+          } else if (this.selectedIndex !== undefined) {
+            e.preventDefault();
+            var expanded = this._files[this.selectedIndex].__expanded;
+            this.set(['_files', this.selectedIndex, '__expanded'], !expanded);
+          }
           break;
         case 40:  // down
         case 74:  // 'j'
@@ -352,7 +365,7 @@
       }
 
       // Don't scroll if it's already in view.
-      if (top > window.pageYOffset + this.topMargin &&
+      if (top > window.pageYOffset &&
           top < window.pageYOffset + window.innerHeight - el.clientHeight) {
         return;
       }
@@ -369,12 +382,13 @@
     },
 
     _computeDiffURL: function(changeNum, patchRange, path) {
+      // @see Issue 4255 regarding double-encoding.
       return '/c/' +
           encodeURIComponent(changeNum) +
           '/' +
           encodeURIComponent(this._patchRangeStr(patchRange)) +
           '/' +
-          path;
+          encodeURIComponent(encodeURIComponent(path));
     },
 
     _patchRangeStr: function(patchRange) {
@@ -395,6 +409,14 @@
       return classes.join(' ');
     },
 
+    _computeShowHideText: function(expanded) {
+      return expanded ? 'â–¼' : 'â—€';
+    },
+
+    _computeHiddenState: function(expanded) {
+      return !expanded;
+    },
+
     _filesChanged: function() {
       this.async(function() {
         var diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff');
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 f61566a..3fe785e 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
@@ -60,16 +60,19 @@
           lines_inserted: 9,
           lines_deleted: 0,
           __path: '/COMMIT_MSG',
+          __expanded: false,
         });
         assert.deepEqual(files[1], {
           lines_inserted: 0,
           lines_deleted: 0,
           __path: 'about.txt',
+          __expanded: false,
         });
         assert.deepEqual(files[2], {
           lines_inserted: 0,
           lines_deleted: 123,
           __path: 'tags.html',
+          __expanded: false,
         });
 
         getChangeFilesStub.restore();
@@ -77,62 +80,77 @@
       });
     });
 
-    test('toggle left diff via shortcut', function() {
-      var toggleLeftDiffStub = sinon.stub();
-      sinon.stub(element, 'diffs', {get: function() {
-        return [{toggleLeftDiff: toggleLeftDiffStub}];
-      }});
-      MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
-      assert.isTrue(toggleLeftDiffStub.calledOnce);
-    });
+    suite('keyboard shortcuts', function() {
+      setup(function() {
+        element._files = [
+          {__path: '/COMMIT_MSG', __expanded: false},
+          {__path: 'file_added_in_rev2.txt', __expanded: false},
+          {__path: 'myfile.txt', __expanded: false},
+        ];
+        element.changeNum = '42';
+        element.patchRange = {
+          basePatchNum: 'PARENT',
+          patchNum: '2',
+        };
+        element.selectedIndex = 0;
+      });
 
-    test('keyboard shortcuts', function() {
-      var toggleInlineDiffsStub = sinon.stub(element, '_toggleInlineDiffs');
-      MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift');  // 'I'
-      assert.isTrue(toggleInlineDiffsStub.calledOnce);
-      toggleInlineDiffsStub.restore();
+      test('toggle left diff via shortcut', function() {
+        var toggleLeftDiffStub = sinon.stub();
+        sinon.stub(element, 'diffs', {get: function() {
+          return [{toggleLeftDiff: toggleLeftDiffStub}];
+        }});
+        MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift');  // 'A'
+        assert.isTrue(toggleLeftDiffStub.calledOnce);
+      });
 
-      element._files = [
-        {__path: '/COMMIT_MSG'},
-        {__path: 'file_added_in_rev2.txt'},
-        {__path: 'myfile.txt'},
-      ];
-      element.changeNum = '42';
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: '2',
-      };
-      element.selectedIndex = 0;
+      test('keyboard shortcuts', function() {
+        var toggleInlineDiffsStub = sinon.stub(element, '_toggleInlineDiffs');
+        MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift');  // 'I'
+        assert.isTrue(toggleInlineDiffsStub.calledOnce);
+        toggleInlineDiffsStub.restore();
 
-      flushAsynchronousOperations();
-      var elementItems = Polymer.dom(element.root).querySelectorAll(
-          '.row:not(.header)');
-      assert.equal(elementItems.length, 3);
-      assert.isTrue(elementItems[0].hasAttribute('selected'));
-      assert.isFalse(elementItems[1].hasAttribute('selected'));
-      assert.isFalse(elementItems[2].hasAttribute('selected'));
-      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'J'
-      assert.equal(element.selectedIndex, 1);
-      MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'J'
+        flushAsynchronousOperations();
+        var elementItems = Polymer.dom(element.root).querySelectorAll(
+            '.row:not(.header)');
+        assert.equal(elementItems.length, 3);
+        assert.isTrue(elementItems[0].hasAttribute('selected'));
+        assert.isFalse(elementItems[1].hasAttribute('selected'));
+        assert.isFalse(elementItems[2].hasAttribute('selected'));
+        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'J'
+        assert.equal(element.selectedIndex, 1);
+        MockInteractions.pressAndReleaseKeyOn(element, 74);  // 'J'
 
-      var showStub = sinon.stub(page, 'show');
-      assert.equal(element.selectedIndex, 2);
-      MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'ENTER'
-      assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
-          'Should navigate to /c/42/2/myfile.txt');
+        var showStub = sinon.stub(page, 'show');
+        assert.equal(element.selectedIndex, 2);
+        MockInteractions.pressAndReleaseKeyOn(element, 13);  // 'ENTER'
+        assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
+            'Should navigate to /c/42/2/myfile.txt');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
-      assert.equal(element.selectedIndex, 1);
-      MockInteractions.pressAndReleaseKeyOn(element, 79);  // 'O'
-      assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
-          'Should navigate to /c/42/2/file_added_in_rev2.txt');
+        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
+        assert.equal(element.selectedIndex, 1);
+        MockInteractions.pressAndReleaseKeyOn(element, 79);  // 'O'
+        assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'),
+            'Should navigate to /c/42/2/file_added_in_rev2.txt');
 
-      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
-      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
-      MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
-      assert.equal(element.selectedIndex, 0);
+        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
+        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
+        MockInteractions.pressAndReleaseKeyOn(element, 75);  // 'K'
+        assert.equal(element.selectedIndex, 0);
 
-      showStub.restore();
+        showStub.restore();
+      });
+
+      test('i key shows/hides selected inline diff', function() {
+        element.selectedIndex = 0;
+        MockInteractions.pressAndReleaseKeyOn(element, 73);  // 'I'
+        assert.isTrue(element._files[0].__expanded);
+        MockInteractions.pressAndReleaseKeyOn(element, 73);  // 'I'
+        assert.isFalse(element._files[0].__expanded);
+        element.selectedIndex = 1;
+        MockInteractions.pressAndReleaseKeyOn(element, 73);  // 'I'
+        assert.isTrue(element._files[1].__expanded);
+      });
     });
 
     test('comment filtering', function() {
@@ -187,9 +205,9 @@
 
     test('file review status', function() {
       element._files = [
-        {__path: '/COMMIT_MSG'},
-        {__path: 'file_added_in_rev2.txt'},
-        {__path: 'myfile.txt'},
+        {__path: '/COMMIT_MSG', __expanded: false},
+        {__path: 'file_added_in_rev2.txt', __expanded: false},
+        {__path: 'myfile.txt', __expanded: false},
       ];
       element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
       element.changeNum = '42';
@@ -202,9 +220,12 @@
       flushAsynchronousOperations();
       var fileRows =
           Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
-      var commitMsg = fileRows[0].querySelector('input[type="checkbox"]');
-      var fileAdded = fileRows[1].querySelector('input[type="checkbox"]');
-      var myFile = fileRows[2].querySelector('input[type="checkbox"]');
+      var commitMsg = fileRows[0].querySelector(
+          'input.reviewed[type="checkbox"]');
+      var fileAdded = fileRows[1].querySelector(
+          'input.reviewed[type="checkbox"]');
+      var myFile = fileRows[2].querySelector(
+          'input.reviewed[type="checkbox"]');
 
       assert.isTrue(commitMsg.checked);
       assert.isFalse(fileAdded.checked);
@@ -267,5 +288,42 @@
         element.fire('change', {}, {node: selectEl});
       });
     });
+
+    test('checkbox shows/hides diff inline', function() {
+      element._files = [
+        {__path: 'myfile.txt', __expanded: false},
+      ];
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
+      element.selectedIndex = 0;
+      flushAsynchronousOperations();
+      var fileRows =
+          Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+      // Prevent diff from making API call.
+      var diffStub = sinon.stub(element.diffs[0], 'reload');
+      var showHideCheck = fileRows[0].querySelector(
+          'input.show-hide[type="checkbox"]');
+      assert.isTrue(showHideCheck.checked);
+      MockInteractions.tap(showHideCheck);
+      assert.isFalse(element.diffs[0].hidden);
+      diffStub.restore();
+    });
+
+    test('file name should be double-escaped', function() {
+      element._files = [
+        {__path: 'my+file.txt'},
+      ];
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
+      flushAsynchronousOperations();
+      assert.equal(
+          element.$$('a').getAttribute('href'), '/c/42/2/my%252Bfile.txt');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 66254d0..958e00f 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -50,12 +50,15 @@
       }
       .showAvatar.collapsed .contentContainer {
         margin-left: calc(var(--default-horizontal-margin) + 1.75em);
-        padding: .75em 2em .75em 0;
       }
       .hideAvatar.collapsed .contentContainer,
       .hideAvatar.expanded .contentContainer {
         margin-left: 0;
-        padding: .75em 2em .75em 0;
+      }
+      .showAvatar.collapsed .contentContainer,
+      .hideAvatar.collapsed .contentContainer,
+      .hideAvatar.expanded .contentContainer {
+        padding: .75em 5em .75em 0;
       }
       .collapsed gr-avatar {
         top: .5em;
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index c92ad07..acb95c1 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -56,15 +56,22 @@
       },
       showReplyButton: {
         type: Boolean,
-        computed: '_computeShowReplyButton(message)',
+        computed: '_computeShowReplyButton(message, _loggedIn)',
       },
       projectConfig: Object,
+      _loggedIn: {
+        type: Boolean,
+        value: false,
+      },
     },
 
     ready: function() {
       this.$.restAPI.getConfig().then(function(config) {
         this.config = config;
       }.bind(this));
+      this.$.restAPI.getLoggedIn().then(function(loggedIn) {
+        this._loggedIn = loggedIn;
+      }.bind(this));
     },
 
     _computeAuthor: function(message) {
@@ -75,8 +82,8 @@
       return !!(author && config && config.plugin && config.plugin.has_avatars);
     },
 
-    _computeShowReplyButton: function(message) {
-      return !!message.message;
+    _computeShowReplyButton: function(message, loggedIn) {
+      return !!message.message && loggedIn;
     },
 
     _commentsChanged: function(value) {
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 c90f58a..ff8610b 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
@@ -85,5 +85,12 @@
       assert.equal(0, content.textContent.trim().indexOf(updatedBy.name));
     });
 
+    test('reply button hidden unless logged in', function() {
+      var message = {
+        'message': 'Uploaded patch set 1.',
+      };
+      assert.isFalse(element._computeShowReplyButton(message, false));
+      assert.isTrue(element._computeShowReplyButton(message, true));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index e7a0573..875bf2b 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -29,7 +29,6 @@
       },
       comments: Object,
       projectConfig: Object,
-      topMargin: Number,
       showReplyButtons: {
         type: Boolean,
         value: false,
@@ -52,7 +51,7 @@
            offsetParent = offsetParent.offsetParent) {
         top += offsetParent.offsetTop;
       }
-      window.scrollTo(0, top - this.topMargin);
+      window.scrollTo(0, top);
       this._highlightEl(el);
     },
 
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 cec1e90..1432d8c 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
@@ -97,9 +97,6 @@
         border: none;
         width: 100%;
       }
-      .labelsNotShown {
-        color: #666;
-      }
       .labelContainer:not(:first-of-type) {
         margin-top: .5em;
       }
@@ -211,28 +208,20 @@
         </iron-autogrow-textarea>
       </section>
       <section class="labelsContainer">
-        <template is="dom-if" if="[[_computeShowLabels(patchNum, revisions)]]">
-          <template is="dom-repeat"
-              items="[[_computeLabelArray(permittedLabels)]]" as="label">
-            <div class="labelContainer">
-              <span class="labelName">[[label]]</span>
-              <iron-selector data-label$="[[label]]"
-                  selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
-                <template is="dom-repeat"
-                    items="[[_computePermittedLabelValues(permittedLabels, label)]]"
-                    as="value">
-                  <gr-button has-tooltip data-value$="[[value]]"
-                      title$="[[_computeLabelValueTitle(labels, label, value)]]">[[value]]</gr-button>
-                </template>
-              </iron-selector>
-            </div>
-          </template>
-        </template>
-        <template is="dom-if" if="[[!_computeShowLabels(patchNum, revisions)]]">
-          <span class="labelsNotShown">
-            Labels are not shown because this is not the most recent patch set.
-            <a href$="/c/[[change._number]]">Go to the latest patch set.</a>
-          </span>
+        <template is="dom-repeat"
+            items="[[_computeLabelArray(permittedLabels)]]" as="label">
+          <div class="labelContainer">
+            <span class="labelName">[[label]]</span>
+            <iron-selector data-label$="[[label]]"
+                selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
+              <template is="dom-repeat"
+                  items="[[_computePermittedLabelValues(permittedLabels, label)]]"
+                  as="value">
+                <gr-button has-tooltip data-value$="[[value]]"
+                    title$="[[_computeLabelValueTitle(labels, label, value)]]">[[value]]</gr-button>
+              </template>
+            </iron-selector>
+          </div>
         </template>
       </section>
       <section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]">
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 d2b279d..b54fc85 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
@@ -48,7 +48,6 @@
     properties: {
       change: Object,
       patchNum: String,
-      revisions: Object,
       disabled: {
         type: Boolean,
         value: false,
@@ -153,8 +152,8 @@
 
         var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
 
-        // The selector may not be present if it’s not at the latest patch set.
-        if (!selectorEl) { continue; }
+        // The user may have not voted on this label.
+        if (!selectorEl.selectedItem) { continue; }
 
         var selectedVal = selectorEl.selectedItem.getAttribute('data-value');
         selectedVal = parseInt(selectedVal, 10);
@@ -259,16 +258,6 @@
       }.bind(this));
     },
 
-    _computeShowLabels: function(patchNum, revisions) {
-      var num = parseInt(patchNum, 10);
-      for (var rev in revisions) {
-        if (revisions[rev]._number > num) {
-          return false;
-        }
-      }
-      return true;
-    },
-
     _computeHideDraftList: function(drafts) {
       return Object.keys(drafts || {}).length == 0;
     },
@@ -295,7 +284,7 @@
         labels, permittedLabels, labelName, account) {
       var t = labels[labelName];
       if (!t) { return null; }
-      var labelValue = t.default_value;
+      var labelValue = null;
 
       // Is there an existing vote for the current user? If so, use that.
       var votes = labels[labelName];
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 8fb4e45..639aeef 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
@@ -107,18 +107,7 @@
       MockInteractions.tap(element.$$('.cancel'));
     });
 
-    test('show/hide labels', function() {
-      var revisions = {
-        rev1: {_number: 1},
-        rev2: {_number: 2},
-      };
-      assert.isFalse(element._computeShowLabels('1', revisions));
-      assert.isTrue(element._computeShowLabels('2', revisions));
-    });
-
     test('label picker', function(done) {
-      var showLabelsStub = sinon.stub(element, '_computeShowLabels',
-          function() { return true; });
       element.revisions = {};
       element.patchNum = '';
 
@@ -156,7 +145,6 @@
               'Element should be enabled when done sending reply.');
           assert.equal(element.draft.length, 0);
           saveReviewStub.restore();
-          showLabelsStub.restore();
           done();
         });
 
@@ -312,7 +300,10 @@
           assert.equal(body, 'first error, second error');
         });
       });
-      element.send().then(done);
+
+      // Async tick is needed because iron-selector content is distributed and
+      // distributed content requires an observer to be set up.
+      flush(function() { element.send().then(done); });
     });
 
     test('ccs are displayed if NoteDb is enabled', function() {
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 7291199..6e88267 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
@@ -155,7 +155,11 @@
           </tr>
           <tr>
             <td><span class="key">a</span></td>
-            <td>Review and publish comments</td>
+            <td>Open reply dialog to publish comments and add reviewers</td>
+          </tr>
+          <tr>
+            <td><span class="key">d</span></td>
+            <td>Open download overlay</td>
           </tr>
           <tr>
             <td></td><td class="header">File list</td>
@@ -173,6 +177,10 @@
             <td>Show selected file</td>
           </tr>
           <tr>
+            <td><span class="key">i</span></td>
+            <td>Show/hide selected inline diff</td>
+          </tr>
+          <tr>
             <td></td><td class="header">Diffs</td>
           </tr>
           <tr>
@@ -297,7 +305,7 @@
           </tr>
           <tr>
             <td><span class="key">a</span></td>
-            <td>Review and publish comments</td>
+            <td>Open reply dialog to publish comments and add reviewers</td>
           </tr>
           <tr>
             <td><span class="key">,</span></td>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 930c8cf..916726fd 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -45,34 +45,38 @@
       .links {
         margin-left: 1em;
       }
-      .links ul {
+      .links .menuContainer {
         display: none;
       }
       .links > li {
         cursor: default;
         display: inline-block;
         margin-left: 1em;
-        padding: .4em 0;
+        padding: .5em 0;
         position: relative;
       }
-      .links li:hover ul {
+      .links li:hover .menuContainer,
+      .links li:active .menuContainer {
         background-color: #fff;
+        border-radius: 3px;
         box-shadow: 0 1px 1px rgba(0, 0, 0, .3);
         display: block;
-        left: -.75em;
+        left: -.5em;
+        padding: .5em 0;
         position: absolute;
-        top: 2em;
+        top: 2.45em;
         z-index: 1000;
       }
       .links li ul li a:link,
       .links li ul li a:visited {
         color: #00e;
         display: block;
-        padding: .5em .75em;
+        padding: .3em 1em;
         text-decoration: none;
         white-space: nowrap;
       }
-      .links li ul li:hover a {
+      .links li ul li:hover a,
+      .links li ul li:active a {
         background-color: var(--selection-background-color);
       }
       .linksTitle {
@@ -87,7 +91,8 @@
         height: 0;
         position: absolute;
         right: 0;
-        top: calc(50% - .1em);
+        top: calc(50% - .05em);
+        transition: border-top-color 200ms;
         width: 0;
       }
       .links li:hover .downArrow {
@@ -137,11 +142,13 @@
             <span class="linksTitle">
               [[linkGroup.title]] <i class="downArrow"></i>
             </span>
-            <ul>
-              <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
-                <li><a href$="[[link.url]]">[[link.name]]</a></li>
-              </template>
-            </ul>
+            <div class="menuContainer">
+              <ul>
+                <template is="dom-repeat" items="[[linkGroup.links]]" as="link">
+                  <li><a href$="[[link.url]]">[[link.name]]</a></li>
+                </template>
+              </ul>
+            </div>
           </li>
         </template>
       </ul>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
new file mode 100644
index 0000000..7653655
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -0,0 +1,21 @@
+<!--
+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-reporting">
+  <script src="gr-reporting.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
new file mode 100644
index 0000000..5236a1c
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -0,0 +1,129 @@
+// 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';
+
+  // Latency reporting constants.
+  var TIMING = {
+    TYPE: 'timing-report',
+    CATEGORY: 'UI Latency',
+    // Reported events - alphabetize below.
+    APP_STARTED: 'App Started',
+    PAGE_LOADED: 'Page Loaded',
+  };
+
+  // Navigation reporting constants.
+  var NAVIGATION = {
+    TYPE: 'nav-report',
+    CATEGORY: 'Location Changed',
+    PAGE: 'Page',
+  };
+
+  var CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/;
+  var DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/;
+
+  Polymer({
+    is: 'gr-reporting',
+
+    properties: {
+      _baselines: {
+        type: Array,
+        value: function() { return {}; },
+      }
+    },
+
+    get performanceTiming() {
+      return window.performance.timing;
+    },
+
+    now: function() {
+      return Math.round(10 * window.performance.now()) / 10;
+    },
+
+    reporter: function(type, category, eventName, eventValue) {
+      eventValue = eventValue;
+      var detail = {
+        type: type,
+        category: category,
+        name: eventName,
+        value: eventValue,
+      };
+      document.dispatchEvent(new CustomEvent(type, {detail: detail}));
+      console.log(eventName + ': ' + eventValue);
+    },
+
+    /**
+     * User-perceived app start time, should be reported when the app is ready.
+     */
+    appStarted: function() {
+      var startTime =
+          new Date().getTime() - this.performanceTiming.navigationStart;
+      this.reporter(
+          TIMING.TYPE, TIMING.CATEGORY, TIMING.APP_STARTED, startTime);
+    },
+
+    /**
+     * Page load time, should be reported at any time after navigation.
+     */
+    pageLoaded: function() {
+      if (this.performanceTiming.loadEventEnd === 0) {
+        console.error('pageLoaded should be called after window.onload');
+        this.async(this.pageLoaded, 100);
+      } else {
+        var loadTime = this.performanceTiming.loadEventEnd -
+            this.performanceTiming.navigationStart;
+        this.reporter(
+          TIMING.TYPE, TIMING.CATEGORY, TIMING.PAGE_LOADED, loadTime);
+      }
+    },
+
+    locationChanged: function() {
+      var page = '';
+      var pathname = this._getPathname();
+      if (pathname.startsWith('/q/')) {
+        page = '/q/';
+      } else if (pathname.match(CHANGE_VIEW_REGEX)) { // change view
+        page = '/c/';
+      } else if (pathname.match(DIFF_VIEW_REGEX)) { // diff view
+        page = '/c//COMMIT_MSG';
+      } else {
+        // Ignore other page changes.
+        return;
+      }
+      this.reporter(
+          NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
+    },
+
+    _getPathname: function() {
+      return window.location.pathname;
+    },
+
+    /**
+     * Reset named timer.
+     */
+    time: function(name) {
+      this._baselines[name] = this.now();
+    },
+
+    /**
+     * Finish named timer and report it to server.
+     */
+    timeEnd: function(name) {
+      var baseTime = this._baselines[name] || 0;
+      var time = this.now() - baseTime;
+      this.reporter(TIMING.TYPE, TIMING.CATEGORY, name, time);
+      delete this._baselines[name];
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
new file mode 100644
index 0000000..b9d07fc
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -0,0 +1,132 @@
+<!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-reporting</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-reporting.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-reporting></gr-reporting>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-reporting tests', function() {
+    var element;
+    var sandbox;
+    var clock;
+    var fakePerformance;
+
+    var NOW_TIME = 100;
+
+    setup(function() {
+      sandbox = sinon.sandbox.create();
+      clock = sinon.useFakeTimers(NOW_TIME);
+      element = fixture('basic');
+      fakePerformance = {
+        navigationStart: 1,
+        loadEventEnd: 2,
+      };
+      sinon.stub(element, 'performanceTiming',
+          {get: function() {return fakePerformance;}});
+      sandbox.stub(element, 'reporter');
+    });
+    teardown(function() {
+      sandbox.restore();
+      clock.restore();
+    });
+
+    test('appStarted', function() {
+      element.appStarted();
+      assert.isTrue(
+          element.reporter.calledWithExactly(
+              'timing-report', 'UI Latency', 'App Started',
+              NOW_TIME - fakePerformance.navigationStart
+      ));
+    });
+
+    test('pageLoaded', function() {
+      element.pageLoaded();
+      assert.isTrue(
+          element.reporter.calledWithExactly(
+              'timing-report', 'UI Latency', 'Page Loaded',
+              fakePerformance.loadEventEnd - fakePerformance.navigationStart)
+      );
+    });
+
+    test('time and timeEnd', function() {
+      var nowStub = sinon.stub(element, 'now').returns(0);
+      element.time('foo');
+      nowStub.returns(1);
+      element.time('bar');
+      nowStub.returns(2);
+      element.timeEnd('bar');
+      nowStub.returns(3.123);
+      element.timeEnd('foo');
+      assert.isTrue(element.reporter.calledWithExactly(
+          'timing-report', 'UI Latency', 'foo', 3.123
+      ));
+      assert.isTrue(element.reporter.calledWithExactly(
+          'timing-report', 'UI Latency', 'bar', 1
+      ));
+    });
+
+    suite('location changed', function() {
+      var pathnameStub;
+      setup(function() {
+        pathnameStub = sinon.stub(element, '_getPathname');
+      });
+
+      teardown(function() {
+        pathnameStub.restore();
+      });
+
+      test('search', function() {
+        pathnameStub.returns('/q/foo');
+        element.locationChanged();
+        assert.isTrue(element.reporter.calledWithExactly(
+            'nav-report', 'Location Changed', 'Page', '/q/'));
+      });
+
+      test('change view', function() {
+        pathnameStub.returns('/c/42/');
+        element.locationChanged();
+        assert.isTrue(element.reporter.calledWithExactly(
+            'nav-report', 'Location Changed', 'Page', '/c/'));
+      });
+
+      test('change view', function() {
+        pathnameStub.returns('/c/41/2');
+        element.locationChanged();
+        assert.isTrue(element.reporter.calledWithExactly(
+            'nav-report', 'Location Changed', 'Page', '/c/'));
+      });
+
+      test('diff view', function() {
+        pathnameStub.returns('/c/41/2/file.txt');
+        element.locationChanged();
+        assert.isTrue(element.reporter.calledWithExactly(
+            'nav-report', 'Location Changed', 'Page', '/c//COMMIT_MSG'));
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
index 2971ed2..4ad2a37 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.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-reporting/gr-reporting.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
index d11d438..d9bf8ac 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -17,9 +17,18 @@
   // 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');
+  if (!app) {
+    console.log('No gr-app found (running tests)');
+    return;
+  }
 
   window.addEventListener('WebComponentsReady', function() {
+    var restAPI = document.createElement('gr-rest-api-interface');
+    var reporting = document.createElement('gr-reporting');
+
+    reporting.timeEnd('WebComponentsReady');
+    reporting.pageLoaded();
+
     // Middleware
     page(function(ctx, next) {
       document.body.scrollTop = 0;
@@ -28,6 +37,7 @@
       // is processed.
       app.async(function() {
         app.fire('location-change');
+        reporting.locationChanged();
       }, 1);
       next();
     });
@@ -124,12 +134,13 @@
       };
       // Don't allow diffing the same patch number against itself.
       if (params.basePatchNum === params.patchNum) {
+        // @see Issue 4255 regarding double-encoding.
         page.redirect('/c/' +
             encodeURIComponent(params.changeNum) +
             '/' +
             encodeURIComponent(params.patchNum) +
             '/' +
-            encodeURIComponent(params.path));
+            encodeURIComponent(encodeURIComponent(params.path)));
         return;
       }
       normalizePatchRangeParams(params);
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index fecb376..2b05d99 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -18,6 +18,8 @@
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
 
 <dom-module id="gr-search-bar">
   <template>
@@ -53,6 +55,7 @@
           multi
           borderless></gr-autocomplete>
       <gr-button id="searchButton">Search</gr-button>
+      <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     </form>
   </template>
   <script src="gr-search-bar.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 8e52f8f..d8121b7 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -78,6 +78,10 @@
     'tr',
   ];
 
+  var MAX_AUTOCOMPLETE_RESULTS = 10;
+
+  var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+
   Polymer({
     is: 'gr-search-bar',
 
@@ -117,41 +121,169 @@
       this._preventDefaultAndNavigateToInputVal(e);
     },
 
+    /**
+     * This function is called in a few different cases:
+     *   - e.target is the search button
+     *   - e.target is the gr-autocomplete widget (#searchInput)
+     *   - e.target is the input element wrapped within #searchInput
+     *
+     * @param {!Event} e
+     */
     _preventDefaultAndNavigateToInputVal: function(e) {
       e.preventDefault();
-      Polymer.dom(e).rootTarget.blur();
-      // @see Issue 4255.
-      page.show('/q/' + encodeURIComponent(encodeURIComponent(this._inputVal)));
+      var target = Polymer.dom(e).rootTarget;
+      // If the target is the #searchInput or has a sub-input component, that
+      // is what holds the focus as opposed to the target from the DOM event.
+      if (target.$.input) {
+        target.$.input.blur();
+      } else {
+        target.blur();
+      }
+      if (this._inputVal) {
+        // @see Issue 4255.
+        page.show('/q/' +
+            encodeURIComponent(encodeURIComponent(this._inputVal)));
+      }
     },
 
-    // TODO(kaspern): Flesh this out better.
-    _makeSuggestion: function(str) {
-      return {
-        name: str,
-        value: str,
-      };
+    /**
+     * Fetch from the API the predicted accounts.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'owner'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'kasp'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchAccounts: function(predicate, expression) {
+      if (expression.length === 0) { return Promise.resolve([]); }
+      return this.$.restAPI.getSuggestedAccounts(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(function(accounts) {
+            if (!accounts) { return []; }
+            return accounts.map(function(acct) {
+              return predicate + ':"' + acct.name + ' <' + acct.email + '>"';
+            });
+      });
     },
 
-    // TODO(kaspern): Expand support for more complicated autocomplete features.
+    /**
+     * Fetch from the API the predicted groups.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'ownerin'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'polyger'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchGroups: function(predicate, expression) {
+      if (expression.length === 0) { return Promise.resolve([]); }
+      return this.$.restAPI.getSuggestedGroups(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(function(groups) {
+            if (!groups) { return []; }
+            var keys = Object.keys(groups);
+            return keys.map(function(key) { return predicate + ':' + key; });
+          });
+    },
+
+    /**
+     * Fetch from the API the predicted projects.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'project'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'gerr'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchProjects: function(predicate, expression) {
+      return this.$.restAPI.getSuggestedProjects(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(function(projects) {
+            if (!projects) { return []; }
+            var keys = Object.keys(projects);
+            return keys.map(function(key) { return predicate + ':' + key; });
+          });
+    },
+
+    /**
+     * Determine what array of possible suggestions should be provided
+     *     to _getSearchSuggestions.
+     * @param {string} input - The full search term, in lowercase.
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchSuggestions: function(input) {
+      // Split the input on colon to get a two part predicate/expression.
+      var splitInput = input.split(':');
+      var predicate = splitInput[0];
+      var expression = splitInput[1] || '';
+      // Switch on the predicate to determine what to autocomplete.
+      switch (predicate) {
+        case 'ownerin':
+        case 'reviewerin':
+          // Fetch groups.
+          return this._fetchGroups(predicate, expression);
+
+        case 'parentproject':
+        case 'project':
+          // Fetch projects.
+          return this._fetchProjects(predicate, expression);
+
+        case 'author':
+        case 'commentby':
+        case 'committer':
+        case 'from':
+        case 'owner':
+        case 'reviewedby':
+        case 'reviewer':
+          // Fetch accounts.
+          return this._fetchAccounts(predicate, expression);
+
+        default:
+          return Promise.resolve(SEARCH_OPERATORS
+              .filter(function(operator) {
+                return operator.indexOf(input) !== -1;
+              }));
+      }
+    },
+
+    /**
+     * Get the sorted, pruned list of suggestions for the current search query.
+     * @param {string} input - The complete search query.
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
     _getSearchSuggestions: function(input) {
-      return Promise.resolve(SEARCH_OPERATORS).then(function(operators) {
-        if (!operators) { return []; }
-        var lowerCaseInput = input
-            .substring(input.lastIndexOf(' ') + 1)
-            .toLowerCase();
-        return operators
-            .filter(function(operator) {
-              // Disallow autocomplete values that exactly match the whole str.
-              var opContainsInput = operator.indexOf(lowerCaseInput) !== -1;
-              var inputContainsOp = lowerCaseInput.indexOf(operator) !== -1;
-              return opContainsInput && !inputContainsOp;
-            })
-            // Prioritize results that start with the input.
-            .sort(function(operator) {
-              return operator.indexOf(lowerCaseInput);
-            })
-            .map(this._makeSuggestion);
-      }.bind(this));
+      // Allow spaces within quoted terms.
+      var tokens = input.match(TOKENIZE_REGEX);
+      var trimmedInput = tokens[tokens.length - 1].toLowerCase();
+
+      return this._fetchSuggestions(trimmedInput)
+          .then(function(operators) {
+            if (!operators) { return []; }
+            return operators
+                // Disallow autocomplete values that exactly match the str.
+                .filter(function(operator) {
+                  return input.indexOf(operator.toLowerCase()) == -1;
+                })
+                // Prioritize results that start with the input.
+                .sort(function(operator) {
+                  return operator.indexOf(trimmedInput);
+                })
+                // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results.
+                .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1)
+                // Map to an object to play nice with gr-autocomplete.
+                .map(function(operator) {
+                  return {
+                    name: operator,
+                    value: operator,
+                  };
+                });
+          });
     },
 
     _handleKey: function(e) {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 0c16774..a6f1817 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -58,6 +58,7 @@
         assert.notEqual(getActiveElement(), element.$.searchButton);
         done();
       });
+      element.value = 'test';
       MockInteractions.tap(element.$.searchButton);
     });
 
@@ -68,6 +69,7 @@
         assert.notEqual(getActiveElement(), element.$.searchButton);
         done();
       });
+      element.value = 'test';
       MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13);
     });
 
@@ -79,22 +81,99 @@
       showStub.restore();
     });
 
-    test('_getSearchSuggestions returns proper set of suggestions',
-        function(done) {
-      element._getSearchSuggestions('is:o')
-          .then(function(suggestions) {
-            assert.equal(suggestions[0].name, 'is:open');
-            assert.equal(suggestions[0].value, 'is:open');
-            assert.equal(suggestions[1].name, 'is:owner');
-            assert.equal(suggestions[1].value, 'is:owner');
-          })
-          .then(function() {
-            element._getSearchSuggestions('asdasdasdasd')
-                .then(function(suggestions) {
-                  assert.equal(suggestions.length, 0);
-                  done();
-                });
+    test('input blurred after commit', function() {
+      var showStub = sinon.stub(page, 'show');
+      var blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur');
+      element.$.searchInput.text = 'fate/stay';
+      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13);
+      assert.isTrue(blurSpy.called);
+      showStub.restore();
+      blurSpy.restore();
+    });
+
+    test('empty search query does not trigger nav', function() {
+      var showSpy = sinon.spy(page, 'show');
+      element.value = '';
+      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13);
+      assert.isFalse(showSpy.called);
+    });
+
+    suite('_getSearchSuggestions',
+        function() {
+      setup(function() {
+        sinon.stub(element.$.restAPI, 'getSuggestedAccounts', function() {
+          return Promise.resolve([
+            {
+              name: 'fred',
+              email: 'fred@goog.co',
+            },
+          ]);
+        });
+        sinon.stub(element.$.restAPI, 'getSuggestedGroups', function() {
+          return Promise.resolve({
+            Polygerrit: 0,
           });
+        });
+        sinon.stub(element.$.restAPI, 'getSuggestedProjects', function() {
+          return Promise.resolve({
+            Polygerrit: 0,
+          });
+        });
+      });
+
+      teardown(function() {
+        element.$.restAPI.getSuggestedAccounts.restore();
+        element.$.restAPI.getSuggestedGroups.restore();
+        element.$.restAPI.getSuggestedProjects.restore();
+      });
+
+      test('Autocompletes accounts',
+          function(done) {
+        return element._getSearchSuggestions('owner:fr')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].value, 'owner:"fred <fred@goog.co>"');
+              done();
+            });
+      });
+
+      test('Autocompletes groups',
+          function(done) {
+        return element._getSearchSuggestions('ownerin:pol')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].value, 'ownerin:Polygerrit');
+              done();
+            });
+      });
+
+      test('Autocompletes projects',
+          function(done) {
+        return element._getSearchSuggestions('project:pol')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].value, 'project:Polygerrit');
+              done();
+            });
+      });
+
+      test('Autocompletes simple searches',
+          function(done) {
+        return element._getSearchSuggestions('is:o')
+            .then(function(suggestions) {
+              assert.equal(suggestions[0].name, 'is:open');
+              assert.equal(suggestions[0].value, 'is:open');
+              assert.equal(suggestions[1].name, 'is:owner');
+              assert.equal(suggestions[1].value, 'is:owner');
+              done();
+            });
+      });
+
+      test('Does not autocomplete with no match',
+          function(done) {
+        return element._getSearchSuggestions('asdasdasdasd')
+            .then(function(suggestions) {
+              assert.equal(suggestions.length, 0);
+              done();
+            });
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index ec19a2d..40a90b7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -14,10 +14,12 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
 <link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
 <link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
 <link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
+
 <dom-module id="gr-diff-builder">
   <template>
     <div class="contentWrapper">
@@ -32,6 +34,7 @@
     <gr-diff-processor
         id="processor"
         groups="{{_groups}}"></gr-diff-processor>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="../gr-diff/gr-diff-line.js"></script>
   <script src="../gr-diff/gr-diff-group.js"></script>
@@ -74,6 +77,7 @@
           _builder: Object,
           _groups: Array,
           _layers: Array,
+          _showTabs: Boolean,
         },
 
         get diffElement() {
@@ -89,6 +93,7 @@
           this._layers = [
             this.$.syntaxLayer,
             this._createIntralineLayer(),
+            this._createTabIndicatorLayer(),
             this.$.rangeLayer,
           ];
 
@@ -99,6 +104,7 @@
 
         render: function(comments, prefs) {
           this.$.syntaxLayer.enabled = prefs.syntax_highlighting;
+          this._showTabs = !!prefs.show_tabs;
 
           // Stop the processor (if it's running).
           this.$.processor.cancel();
@@ -111,17 +117,19 @@
 
           this._clearDiffContent();
 
-          console.time(TimingLabel.TOTAL);
-          console.time(TimingLabel.CONTENT);
+          var reporting = this.$.reporting;
+
+          reporting.time(TimingLabel.TOTAL);
+          reporting.time(TimingLabel.CONTENT);
           return this.$.processor.process(this.diff.content).then(function() {
             if (this.isImageDiff) {
               this._builder.renderDiffImages();
             }
-            console.timeEnd(TimingLabel.CONTENT);
-            console.time(TimingLabel.SYNTAX);
+            reporting.timeEnd(TimingLabel.CONTENT);
+            reporting.time(TimingLabel.SYNTAX);
             this.$.syntaxLayer.process().then(function() {
-              console.timeEnd(TimingLabel.SYNTAX);
-              console.timeEnd(TimingLabel.TOTAL);
+              reporting.timeEnd(TimingLabel.SYNTAX);
+              reporting.timeEnd(TimingLabel.TOTAL);
             });
             this.fire('render');
           }.bind(this));
@@ -325,6 +333,31 @@
           };
         },
 
+        _createTabIndicatorLayer: function() {
+          var show = (function() { return this._showTabs; }).bind(this);
+          return {
+            addListener: function() {},
+            annotate: function(el, line) {
+              // If visible tabs are disabled, do nothing.
+              if (!show()) { return; }
+
+              // Find and annotate the locations of tabs.
+              var split = line.text.split('\t');
+              if (!split) { return; }
+              for (var i = 0, pos = 0; i < split.length - 1; i++) {
+                // Skip forward by the length of the content
+                pos += split[i].length;
+
+                GrAnnotation.annotateElement(el, pos, 1,
+                    'style-scope gr-diff tab-indicator');
+
+                // Skip forward by one tab character.
+                pos++;
+              }
+            },
+          };
+        },
+
         /**
          * In pages with large diffs, creating the first comment thread can be
          * slow because nested Polymer elements (particularly
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 2090e98..6844416 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -340,6 +340,9 @@
         side,
         this._comments.meta.projectConfig);
     threadEl.comments = comments;
+    if (opt_side) {
+      threadEl.setAttribute('data-side', opt_side);
+    }
     return threadEl;
   };
 
@@ -363,11 +366,14 @@
 
   GrDiffBuilder.prototype._createTextEl = function(line, opt_side) {
     var td = this._createElement('td');
+    var text = line.text;
     if (line.type !== GrDiffLine.Type.BLANK) {
       td.classList.add('content');
+      if (!text) {
+        text = '\xa0';
+      }
     }
     td.classList.add(line.type);
-    var text = line.text;
     var html = util.escapeHTML(text);
     html = this._addTabWrappers(html, this._prefs.tab_size);
 
@@ -493,7 +499,7 @@
     for (var i = 0; i < split.length - 1; i++) {
       offset += split[i].length;
       width = tabSize - (offset % tabSize);
-      result += split[i] + this._getTabWrapper(width, this._prefs.show_tabs);
+      result += split[i] + this._getTabWrapper(width);
       offset += width;
     }
     if (split.length) {
@@ -503,7 +509,7 @@
     return result;
   };
 
-  GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) {
+  GrDiffBuilder.prototype._getTabWrapper = function(tabSize) {
     // Force this to be a number to prevent arbitrary injection.
     tabSize = +tabSize;
     if (isNaN(tabSize)) {
@@ -511,9 +517,6 @@
     }
 
     var str = '<span class="style-scope gr-diff tab ';
-    if (showTabs) {
-      str += 'withIndicator';
-    }
     str += '" style="';
     // TODO(andybons): CSS tab-size is not supported in IE.
     str += 'tab-size:' + tabSize + ';';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index e8b1453..af44629 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -414,6 +414,117 @@
       });
     });
 
+    suite('tab indicators', function() {
+      var sandbox;
+      var element;
+      var layer;
+
+      setup(function() {
+        sandbox = sinon.sandbox.create();
+        element = fixture('basic');
+        element._showTabs = true;
+        layer = element._createTabIndicatorLayer();
+      });
+
+      teardown(function() {
+        sandbox.restore();
+      });
+
+      test('does nothing with empty line', function() {
+        var line = {text: ''};
+        var el = document.createElement('div');
+        var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+        layer.annotate(el, line);
+
+        assert.isFalse(annotateElementStub.called);
+      });
+
+      test('does nothing with no tabs', function() {
+        var str = 'lorem ipsum no tabs';
+        var line = {text: str};
+        var el = document.createElement('div');
+        el.textContent = str;
+        var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+        layer.annotate(el, line);
+
+        assert.isFalse(annotateElementStub.called);
+      });
+
+      test('annotates tab at beginning', function() {
+        var str = '\tlorem upsum';
+        var line = {text: str};
+        var el = document.createElement('div');
+        el.textContent = str;
+        var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+        layer.annotate(el, line);
+
+        assert.equal(annotateElementStub.callCount, 1);
+        var args = annotateElementStub.getCalls()[0].args;
+        assert.equal(args[0], el);
+        assert.equal(args[1], 0, 'offset of tab indicator');
+        assert.equal(args[2], 1, 'length of tab indicator');
+        assert.include(args[3], 'tab-indicator');
+      });
+
+      test('does not annotate when disabled', function() {
+        element._showTabs = false;
+
+        var str = '\tlorem upsum';
+        var line = {text: str};
+        var el = document.createElement('div');
+        el.textContent = str;
+        var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+        layer.annotate(el, line);
+
+        assert.isFalse(annotateElementStub.called);
+      });
+
+      test('annotates multiple in beginning', function() {
+        var str = '\t\tlorem upsum';
+        var line = {text: str};
+        var el = document.createElement('div');
+        el.textContent = str;
+        var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+        layer.annotate(el, line);
+
+        assert.equal(annotateElementStub.callCount, 2);
+
+        var args = annotateElementStub.getCalls()[0].args;
+        assert.equal(args[0], el);
+        assert.equal(args[1], 0, 'offset of tab indicator');
+        assert.equal(args[2], 1, 'length of tab indicator');
+        assert.include(args[3], 'tab-indicator');
+
+        args = annotateElementStub.getCalls()[1].args;
+        assert.equal(args[0], el);
+        assert.equal(args[1], 1, 'offset of tab indicator');
+        assert.equal(args[2], 1, 'length of tab indicator');
+        assert.include(args[3], 'tab-indicator');
+      });
+
+      test('annotates intermediate tabs', function() {
+        var str = 'lorem\tupsum';
+        var line = {text: str};
+        var el = document.createElement('div');
+        el.textContent = str;
+        var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement');
+
+        layer.annotate(el, line);
+
+        assert.equal(annotateElementStub.callCount, 1);
+        var args = annotateElementStub.getCalls()[0].args;
+        assert.equal(args[0], el);
+        assert.equal(args[1], 5, 'offset of tab indicator');
+        assert.equal(args[2], 1, 'length of tab indicator');
+        assert.include(args[3], 'tab-indicator');
+      });
+    });
+
     suite('rendering', function() {
       var content;
       var outputEl;
@@ -437,6 +548,10 @@
             ]
           },
         ];
+        stub('gr-reporting', {
+          time: sinon.stub(),
+          timeEnd: sinon.stub(),
+        });
         element = fixture('basic');
         outputEl = element.queryEffectiveChildren('#diffTable');
         element.addEventListener('render', function() {
@@ -458,6 +573,20 @@
         element.render({left: [], right: []}, prefs);
       });
 
+      test('reporting', function(done) {
+        var timeStub = element.$.reporting.time;
+        var timeEndStub = element.$.reporting.timeEnd;
+        flush(function() {
+          assert.isTrue(timeStub.calledWithExactly('Diff Total Render'));
+          assert.isTrue(timeStub.calledWithExactly('Diff Content Render'));
+          assert.isTrue(timeStub.calledWithExactly('Diff Syntax Render'));
+          assert.isTrue(timeEndStub.calledWithExactly('Diff Total Render'));
+          assert.isTrue(timeEndStub.calledWithExactly('Diff Content Render'));
+          assert.isTrue(timeEndStub.calledWithExactly('Diff Syntax Render'));
+          done();
+        });
+      });
+
       test('renderSection', function() {
         var section = outputEl.querySelector('stub:nth-of-type(2)');
         var prevInnerHTML = section.innerHTML;
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 1b30bde..07badbf 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
@@ -198,8 +198,15 @@
     },
 
     _handleTextareaKeydown: function(e) {
-      if (e.keyCode == 27) {  // 'esc'
-        this._handleCancel(e);
+      switch (e.keyCode) {
+        case 27: // 'esc'
+          this._handleCancel(e);
+          break;
+        case 83: // 's'
+          if (e.ctrlKey) {
+            this._handleSave(e);
+          }
+          break;
       }
     },
 
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 fcf8b41..bddc3ab 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
@@ -211,6 +211,18 @@
       MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc
     });
 
+    test('ctrl+s saves comment', function(done) {
+      var stub = sinon.stub(element, 'save', function() {
+        assert.isTrue(stub.called);
+        stub.restore();
+        done();
+      });
+      element._messageText = 'is that the horse from horsing around??';
+      MockInteractions.pressAndReleaseKeyOn(
+        element.$.editTextarea.textarea,
+        83, 'ctrl');  // 'ctrl + s'
+    });
+
     test('draft saving/editing', function(done) {
       var fireStub = sinon.stub(element, 'fire');
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
index 5a41709..491eded 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
@@ -23,7 +23,6 @@
         id="cursorManager"
         scroll="keep-visible"
         cursor-target-class="target-row"
-        fold-offset-top="[[foldOffsetTop]]"
         target="{{diffRow}}"></gr-cursor-manager>
   </template>
   <script src="gr-diff-cursor.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index 99a0b5c..dd11f2c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -54,11 +54,6 @@
         },
       },
 
-      foldOffsetTop: {
-        type: Number,
-        value: 0,
-      },
-
       /**
        * If set, the cursor will attempt to move to the line number (instead of
        * the first chunk) the next time the diff renders. It is set back to null
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index bfe103b..62e58ad 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -94,6 +94,43 @@
       }
     },
 
+    _getContentTextParent: function(target) {
+      var element = target;
+      if (element.nodeName === '#text') {
+        element = element.parentElement;
+      }
+      while (!element.classList.contains('contentText')) {
+        if (element.parentElement === null) {
+          return target;
+        }
+        element = element.parentElement;
+      }
+      return element;
+    },
+
+    /**
+     * Remap DOM range to whole lines of a diff if necessary. If the start or
+     * end containers are DOM elements that are singular pieces of syntax
+     * highlighting, the containers are remapped to the .contentText divs that
+     * contain the entire line of code.
+     *
+     * @param  {Object} range - the standard DOM selector range.
+     * @return {Object} A modified version of the range that correctly accounts
+     *     for syntax highlighting.
+     */
+    _normalizeRange: function(range) {
+      var startContainer = this._getContentTextParent(range.startContainer);
+      var startOffset = range.startOffset + this._getTextOffset(startContainer,
+          range.startContainer);
+      var endContainer = this._getContentTextParent(range.endContainer);
+      var endOffset = range.endOffset + this._getTextOffset(endContainer,
+          range.endContainer);
+      return {
+        start: this._normalizeSelectionSide(startContainer, startOffset),
+        end: this._normalizeSelectionSide(endContainer, endOffset),
+      };
+    },
+
     /**
      * Convert DOM Range selection to concrete numbers (line, column, side).
      * Moves range end if it's not inside td.content.
@@ -160,13 +197,12 @@
       if (range.collapsed) {
         return;
       }
-      var start =
-          this._normalizeSelectionSide(range.startContainer, range.startOffset);
+      var normalizedRange = this._normalizeRange(range);
+      var start = normalizedRange.start;
       if (!start) {
         return;
       }
-      var end =
-          this._normalizeSelectionSide(range.endContainer, range.endOffset);
+      var end = normalizedRange.end;
       if (!end) {
         return;
       }
@@ -270,5 +306,36 @@
         return GrAnnotation.getLength(node);
       }
     },
+
+    /**
+     * Gets the character offset of the child within the parent.
+     * Performs a synchronous in-order traversal from top to bottom of the node
+     * element, counting the length of the syntax until child is found.
+     *
+     * @param {!Element} The root DOM element to be searched through.
+     * @param {!Element} The child element being searched for.
+     * @return {number}
+     */
+    _getTextOffset: function(node, child) {
+      var count = 0;
+      var stack = [node];
+      while (stack.length) {
+        var n = stack.pop();
+        if (n === child) {
+          break;
+        }
+        if (n.childNodes && n.childNodes.length !== 0) {
+          var arr = [];
+          for (var i = 0; i < n.childNodes.length; i++) {
+            arr.push(n.childNodes[i]);
+          }
+          arr.reverse();
+          stack = stack.concat(arr);
+        } else {
+          count += this._getLength(n);
+        }
+      }
+      return count;
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 5f84e4f..2612f9a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -487,6 +487,27 @@
         assert.equal(getActionSide(), 'left');
       });
 
+      test('properly accounts for syntax highlighting', function() {
+        var content = stubContent(140, 'left');
+        var spy = sinon.spy(element, '_normalizeRange');
+        emulateSelection(
+            content.querySelectorAll('hl')[3], 0,
+            content.querySelectorAll('span')[1], 0);
+        var spyCall = spy.getCall(0);
+        var range = window.getSelection().getRangeAt(0);
+        assert.notDeepEqual(spyCall.returnValue, range);
+      });
+
+      test('_getTextOffset computes text offset', function() {
+        var content = stubContent(140, 'left');
+        var child = content.lastChild.lastChild;
+        var result = element._getTextOffset(content, child);
+        assert.equal(result, 73);
+        content = stubContent(146, 'right');
+        child = content.lastChild;
+        result = element._getTextOffset(content, child);
+        assert.equal(result, 0);
+      });
       // TODO (viktard): Selection starts in line number.
       // TODO (viktard): Empty lines in selection start.
       // TODO (viktard): Empty lines in selection end.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index 2a1e880..2dd4c91 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -78,6 +78,22 @@
       },
 
       _nextStepHandle: Number,
+      _isScrolling: Boolean,
+    },
+
+    attached: function() {
+      this.listen(window, 'scroll', '_handleWindowScroll');
+    },
+
+    detached: function() {
+      this.unlisten(window, 'scroll', '_handleWindowScroll');
+    },
+
+    _handleWindowScroll: function() {
+      this._isScrolling = true;
+      this.debounce('resetIsScrolling', function() {
+        this._isScrolling = false;
+      }, 50);
     },
 
     /**
@@ -100,6 +116,11 @@
 
         var currentBatch = 0;
         var nextStep = function() {
+
+          if (this._isScrolling) {
+            this.async(nextStep, 100);
+            return;
+          }
           // If we are done, resolve the promise.
           if (state.sectionIndex >= content.length) {
             resolve(this.groups);
@@ -201,11 +222,11 @@
     /**
      * Take rows of a shared diff section and produce an array of corresponding
      * (potentially collapsed) groups.
-     * @param  {Array<String>} rows
-     * @param  {Number} context
-     * @param  {Number} startLineNumLeft
-     * @param  {Number} startLineNumRight
-     * @param  {String} opt_sectionEnd String representing whether this is the
+     * @param {Array<String>} rows
+     * @param {Number} context
+     * @param {Number} startLineNumLeft
+     * @param {Number} startLineNumRight
+     * @param {String} opt_sectionEnd String representing whether this is the
      *     first section or the last section or neither. Use the values 'first',
      *     'last' and null respectively.
      * @return {Array<GrDiffGroup>}
@@ -264,10 +285,10 @@
     /**
      * Take the rows of a delta diff section and produce the corresponding
      * group.
-     * @param  {Array<String>} rowsAdded
-     * @param  {Array<String>} rowsRemoved
-     * @param  {Number} startLineNumLeft
-     * @param  {Number} startLineNumRight
+     * @param {Array<String>} rowsAdded
+     * @param {Array<String>} rowsRemoved
+     * @param {Number} startLineNumLeft
+     * @param {Number} startLineNumRight
      * @return {GrDiffGroup}
      */
     _deltaGroupFromRows: function(rowsAdded, rowsRemoved, startLineNumLeft,
@@ -325,7 +346,7 @@
      * 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.
-     * @param  {Object} content The diff content object.
+     * @param {Object} content The diff content object.
      * @return {Object} A new diff content object with regions split up.
      */
     _splitCommonGroupsWithComments: function(content) {
@@ -477,8 +498,8 @@
     /**
      * Given an array and a size, return an array of arrays where no inner array
      * is larger than that size, preserving the original order.
-     * @param  {!Array<T>}
-     * @param  {number}
+     * @param {!Array<T>} array
+     * @param {number} size
      * @return {!Array<!Array<T>>}
      * @template T
      */
@@ -489,7 +510,7 @@
       var head = array.slice(0, array.length - size);
       var tail = array.slice(array.length - size);
 
-      return this._breakdown(head, size).concat([tail])
+      return this._breakdown(head, size).concat([tail]);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 9d687ac..4f8c532 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -40,6 +40,15 @@
         'fugit assum per.';
 
     var element;
+    var sandbox;
+
+    setup(function() {
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(function() {
+      sandbox.restore();
+    });
 
     suite('not logged in', function() {
 
@@ -409,6 +418,23 @@
         ]);
       });
 
+      test('scrolling pauses rendering', function() {
+        var contentRow = {
+          ab: [
+            '<!DOCTYPE html>',
+            '<meta charset="utf-8">',
+          ]
+        };
+        var content = _.times(200, _.constant(contentRow));
+        sandbox.stub(element, 'async');
+        element._isScrolling = true;
+        element.process(content);
+        assert.equal(element.groups.length, 1);
+        element._isScrolling = false;
+        element.process(content);
+        assert.equal(element.groups.length, 33);
+      });
+
       suite('gr-diff-processor helpers', function() {
         var rows;
 
@@ -512,15 +538,6 @@
       });
 
       suite('_breakdown*', function() {
-        var sandbox;
-        setup(function() {
-          sandbox = sinon.sandbox.create();
-        });
-
-        teardown(function() {
-          sandbox.restore();
-        });
-
         test('_breakdownGroup ignores shared groups', function() {
           sandbox.stub(element, '_breakdown');
           var chunk = {ab: ['blah', 'blah', 'blah']};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
index 09cab0b..3c24291 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
@@ -18,17 +18,21 @@
 <dom-module id="gr-diff-selection">
   <template>
     <style>
-      .contentWrapper ::content .content {
+      .contentWrapper ::content .content,
+      .contentWrapper ::content .contextControl {
         -webkit-user-select: none;
         -moz-user-select: none;
         -ms-user-select: none;
         user-select: none;
       }
 
-      :host.selected-right .contentWrapper ::content .right + .content,
-      :host.selected-left .contentWrapper ::content .left + .content,
-      :host.selected-right .contentWrapper ::content .unified .right ~ .content,
-      :host.selected-left .contentWrapper ::content .unified .left ~ .content {
+      :host-context(.selected-left:not(.selected-comment)) .contentWrapper ::content .side-by-side .left + .content .contentText,
+      :host-context(.selected-right:not(.selected-comment)) .contentWrapper ::content .side-by-side .right + .content .contentText,
+      :host-context(.selected-left:not(.selected-comment)) .contentWrapper ::content .unified .left.lineNum ~ .content:not(.both) .contentText,
+      :host-context(.selected-right:not(.selected-comment)) .contentWrapper ::content .unified .right.lineNum ~ .content .contentText,
+      :host-context(.selected-left.selected-comment) .contentWrapper ::content .side-by-side .left + .content .message,
+      :host-context(.selected-right.selected-comment) .contentWrapper ::content .side-by-side .right + .content .message,
+      :host-context(.selected-comment) .contentWrapper ::content .unified .message {
         -webkit-user-select: text;
         -moz-user-select: text;
         -ms-user-select: text;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 7d0b7ea..35da901 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -18,7 +18,12 @@
     is: 'gr-diff-selection',
 
     properties: {
+      diff: Object,
       _cachedDiffBuilder: Object,
+      _linesCache: {
+        type: Object,
+        value: function() { return {left: null, right: null}; },
+      },
     },
 
     listeners: {
@@ -56,8 +61,12 @@
     },
 
     _handleCopy: function(e) {
-      if (!e.target.classList.contains('content')) {
-        return;
+      var el = e.target;
+      while (!el.classList.contains('content')) {
+        if (!el.parentElement) {
+          return;
+        }
+        el = el.parentElement;
       }
       var lineEl = this.diffBuilder.getLineElByChild(e.target);
       if (!lineEl) {
@@ -69,27 +78,53 @@
       e.preventDefault();
     },
 
-    _getSelectedText: function(opt_side) {
+    _getSelectedText: function(side) {
       var sel = window.getSelection();
       if (sel.rangeCount != 1) {
         return; // No multi-select support yet.
       }
       var range = sel.getRangeAt(0);
-      var fragment = range.cloneContents();
-      var selector = '.content,td.content:nth-of-type(1)';
-      if (opt_side) {
-        selector = '.' + opt_side + ' + ' + selector;
+      var startLineEl = this.diffBuilder.getLineElByChild(range.startContainer);
+      var endLineEl = this.diffBuilder.getLineElByChild(range.endContainer);
+      var startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10);
+      var endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10);
+
+      return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum,
+          range.endOffset, side);
+    },
+
+    _getRangeFromDiff: function(startLineNum, startOffset, endLineNum,
+        endOffset, side) {
+      var lines = this._getDiffLines(side).slice(startLineNum - 1, endLineNum);
+      if (lines.length) {
+        lines[0] = lines[0].substring(startOffset);
+        lines[lines.length - 1] = lines[lines.length - 1]
+            .substring(0, endOffset);
       }
-      var contentEls = Polymer.dom(fragment).querySelectorAll(selector);
-      if (contentEls.length === 0) {
-        return fragment.textContent;
+      return lines.join('\n');
+    },
+
+    _getDiffLines: function(side) {
+      if (this._linesCache[side]) {
+        return this._linesCache[side];
       }
 
-      var text = '';
-      for (var i = 0; i < contentEls.length; i++) {
-        text += contentEls[i].textContent + '\n';
+      var lines = [];
+      var chunk;
+      var key = side === 'left' ? 'a' : 'b';
+      for (var chunkIndex = 0;
+          chunkIndex < this.diff.content.length;
+          chunkIndex++) {
+        chunk = this.diff.content[chunkIndex];
+        if (chunk.ab) {
+          lines = lines.concat(chunk.ab);
+        } else if (chunk[key]) {
+          lines = lines.concat(chunk[key]);
+        }
       }
-      return text;
+
+      this._linesCache[side] = lines;
+      return lines;
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index f99e373..cdc7483 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -27,24 +27,36 @@
 <test-fixture id="basic">
   <template>
     <gr-diff-selection>
-      <table>
+      <table class="side-by-side">
         <tr>
-          <td class="lineNum left">1</td>
-          <td class="content">ba ba</td>
-          <td class="lineNum right">1</td>
-          <td class="content">some other text</td>
+          <td class="lineNum left" data-value="1">1</td>
+          <td class="content">
+            <div class="contentText" data-side="left">ba ba</div>
+          </td>
+          <td class="lineNum right" data-value="1">1</td>
+          <td class="content">
+            <div class="contentText" data-side="right">some other text</div>
+          </td>
         </tr>
         <tr>
-          <td class="lineNum left">2</td>
-          <td class="content">zin</td>
-          <td class="lineNum right">2</td>
-          <td class="content">more more more</td>
+          <td class="lineNum left" data-value="2">2</td>
+          <td class="content">
+            <div class="contentText" data-side="left">zin</div>
+          </td>
+          <td class="lineNum right" data-value="2">2</td>
+          <td class="content">
+            <div class="contentText" data-side="right">more more more</div>
+          </td>
         </tr>
         <tr>
-          <td class="lineNum left">2</td>
-          <td class="content">ga ga</td>
-          <td class="lineNum right">3</td>
-          <td class="other">some other text</td>
+          <td class="lineNum left" data-value="3">3</td>
+          <td class="content">
+            <div class="contentText" data-side="left">ga ga</div>
+          </td>
+          <td class="lineNum right" data-value="3">3</td>
+          <td class="other">
+            <div class="contentText" data-side="right">some other text</div>
+          </td>
         </tr>
       </table>
     </gr-diff-selection>
@@ -73,6 +85,22 @@
         getLineElByChild: sinon.stub().returns({}),
         getSideByLineEl: sinon.stub(),
       };
+      element.diff = {
+        content: [
+          {
+            a: ['ba ba'],
+            b: ['some other text'],
+          },
+          {
+            a: ['zin'],
+            b: ['more more more'],
+          },
+          {
+            a: ['ga ga'],
+            b: ['some other text'],
+          },
+        ],
+      };
     });
 
     test('applies selected-left on left side click', function() {
@@ -105,38 +133,43 @@
     test('asks for text for right side Elements', function() {
       element._cachedDiffBuilder.getSideByLineEl.returns('left');
       sinon.stub(element, '_getSelectedText');
-      emulateCopyOn(element.querySelector('td.content'));
+      emulateCopyOn(element.querySelector('div.contentText'));
       assert.deepEqual(['left'], element._getSelectedText.lastCall.args);
     });
 
     test('reacts to copy for content Elements', function() {
       sinon.stub(element, '_getSelectedText');
-      emulateCopyOn(element.querySelector('td.content'));
+      emulateCopyOn(element.querySelector('div.contentText'));
       assert.isTrue(element._getSelectedText.called);
     });
 
     test('copy event is prevented for content Elements', function() {
       sinon.stub(element, '_getSelectedText');
-      var event = emulateCopyOn(element.querySelector('td.content'));
+      var event = emulateCopyOn(element.querySelector('div.contentText'));
       assert.isTrue(event.preventDefault.called);
     });
 
     test('inserts text into clipboard on copy', function() {
       sinon.stub(element, '_getSelectedText').returns('the text');
-      var event = emulateCopyOn(element.querySelector('td.content'));
+      var event = emulateCopyOn(element.querySelector('div.contentText'));
       assert.deepEqual(
           ['Text', 'the text'], event.clipboardData.setData.lastCall.args);
     });
 
     test('copies content correctly', function() {
       element.classList.add('selected-left');
+      element.classList.remove('selected-right');
+      // Fetch the line number.
+      element._cachedDiffBuilder.getLineElByChild = function(child) {
+        return child.parentElement.parentElement.previousElementSibling;
+      };
       var selection = window.getSelection();
       var range = document.createRange();
-      range.setStart(element.querySelector('td.content').firstChild, 3);
+      range.setStart(element.querySelector('div.contentText').firstChild, 3);
       range.setEnd(
-          element.querySelectorAll('td.content')[4].firstChild, 2);
+          element.querySelectorAll('div.contentText')[4].firstChild, 2);
       selection.addRange(range);
-      assert.equal('ba\nzin\nga\n', element._getSelectedText('left'));
+      assert.equal(element._getSelectedText('left'), 'ba\nzin\nga');
     });
   });
 </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 2573ad1..76f7aec 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
@@ -33,9 +33,18 @@
         background-color: var(--view-background-color);
         display: block;
       }
-      h3 {
+      header,
+      .subHeader {
+        align-items: center;
+        display: flex;
+        justify-content: space-between;
+      }
+      header {
         padding: .75em var(--default-horizontal-margin);
       }
+      .navLink:not([href]) {
+        color: #999;
+      }
       .reviewed {
         display: inline-block;
         margin: 0 .25em;
@@ -97,10 +106,7 @@
         padding: 0 var(--default-horizontal-margin) 1em;
         color: #666;
       }
-      .header {
-        align-items: center;
-        display: flex;
-        justify-content: space-between;
+      .subHeader {
         margin: 0 var(--default-horizontal-margin) .75em;
       }
       .prefsButton {
@@ -125,49 +131,56 @@
         }
       }
     </style>
-    <h3>
-      <a href$="[[_computeChangePath(_changeNum, _patchRange.*, _change.revisions)]]">
-        [[_changeNum]]</a><span>:</span>
-      <span>[[_change.subject]]</span>
-      <span class="dash">—</span>
-      <input id="reviewed"
-          class="reviewed"
-          type="checkbox"
-          on-change="_handleReviewedChange"
-          hidden$="[[!_loggedIn]]" hidden>
-      <div class="jumpToFileContainer">
-        <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
-          <span>[[_computeFileDisplayName(_path)]]</span>
-          <span class="downArrow">&#9660;</span>
-        </gr-button>
-        <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
-          <div class="dropdown-content">
+    <header>
+      <h3>
+        <a href$="[[_computeChangePath(_changeNum, _patchRange.*, _change.revisions)]]">
+          [[_changeNum]]</a><span>:</span>
+        <span>[[_change.subject]]</span>
+        <span class="dash">—</span>
+        <input id="reviewed"
+            class="reviewed"
+            type="checkbox"
+            on-change="_handleReviewedChange"
+            hidden$="[[!_loggedIn]]" hidden>
+        <div class="jumpToFileContainer">
+          <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
+            <span>[[_computeFileDisplayName(_path)]]</span>
+            <span class="downArrow">&#9660;</span>
+          </gr-button>
+          <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
+            <div class="dropdown-content">
+              <template is="dom-repeat" items="[[_fileList]]" as="path">
+                <a href$="[[_computeDiffURL(_changeNum, _patchRange.*, path)]]"
+                   selected$="[[_computeFileSelected(path, _path)]]"
+                   data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
+                   on-tap="_handleFileTap">[[_computeFileDisplayName(path)]]</a>
+              </template>
+            </div>
+          </iron-dropdown>
+        </div>
+        <div class="mobileJumpToFileContainer">
+          <select on-change="_handleMobileSelectChange">
             <template is="dom-repeat" items="[[_fileList]]" as="path">
-              <a href$="[[_computeDiffURL(_changeNum, _patchRange.*, path)]]"
-                 selected$="[[_computeFileSelected(path, _path)]]"
-                 data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
-                 on-tap="_handleFileTap">
-                 [[_computeFileDisplayName(path)]]
-              </a>
+              <option
+                  value$="[[path]]"
+                  selected$="[[_computeFileSelected(path, _path)]]">
+                [[_computeFileDisplayName(path)]]
+              </option>
             </template>
-          </div>
-        </iron-dropdown>
+          </select>
+        </div>
+      </h3>
+      <div>
+        <a class="navLink"
+            href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]">Prev</a>
+        /
+        <a class="navLink"
+            href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">Next</a>
       </div>
-      <div class="mobileJumpToFileContainer">
-        <select on-change="_handleMobileSelectChange">
-          <template is="dom-repeat" items="[[_fileList]]" as="path">
-            <option
-                value$="[[path]]"
-                selected$="[[_computeFileSelected(path, _path)]]">
-              [[_computeFileDisplayName(path)]]
-            </option>
-          </template>
-        </select>
-      </div>
-    </h3>
+    </header>
     <div class="loading" hidden$="[[!_loading]]">Loading...</div>
     <div hidden$="[[_loading]]" hidden>
-      <div class="header">
+      <div class="subHeader">
         <gr-patch-range-select
             path="[[_path]]"
             change-num="[[_changeNum]]"
@@ -180,7 +193,8 @@
               id="modeSelect"
               is="gr-select"
               bind-value="{{changeViewState.diffMode}}"
-              hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">
+              hidden$="[[_computeModeSelectHidden(_isImageDiff)]]"
+              on-change="_handleDropdownChange">
             <option value="SIDE_BY_SIDE">Side By Side</option>
             <option value="UNIFIED_DIFF">Unified</option>
           </select>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index d6a3bc0..40b0ee1 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
@@ -105,6 +105,15 @@
         }
       }.bind(this));
 
+      if (this.changeViewState.diffMode === null) {
+        // Initialize with user's diff mode preference. Default to
+        // SIDE_BY_SIDE in the meantime.
+        this.set('changeViewState.diffMode', DiffViewMode.SIDE_BY_SIDE);
+        this.$.restAPI.getPreferences().then(function(prefs) {
+          this.set('changeViewState.diffMode', prefs.diff_view);
+        }.bind(this));
+      }
+
       if (this._path) {
         this.fire('title-change',
             {title: this._computeFileDisplayName(this._path)});
@@ -113,11 +122,6 @@
       this.$.cursor.push('diffs', this.$.diff);
     },
 
-    detached: function() {
-      // Reset the diff mode to null so that it reverts to the user preference.
-      this.changeViewState.diffMode = null;
-    },
-
     _getLoggedIn: function() {
       return this.$.restAPI.getLoggedIn();
     },
@@ -207,11 +211,11 @@
           break;
         case 219:  // '['
           e.preventDefault();
-          this._navToFile(this._fileList, -1);
+          this._navToFile(this._path, this._fileList, -1);
           break;
         case 221:  // ']'
           e.preventDefault();
-          this._navToFile(this._fileList, 1);
+          this._navToFile(this._path, this._fileList, 1);
           break;
         case 78:  // 'n'
           e.preventDefault();
@@ -256,20 +260,41 @@
       }
     },
 
-    _navToFile: function(fileList, direction) {
-      if (fileList.length == 0) { return; }
+    _navToFile: function(path, fileList, direction) {
+      var url = this._computeNavLinkURL(path, fileList, direction);
+      if (!url) { return; }
 
-      var idx = fileList.indexOf(this._path) + direction;
+      page.show(this._computeNavLinkURL(path, fileList, direction));
+    },
+
+    /**
+     * @param {?string} path The path of the current file being shown.
+     * @param {Array.<string>} fileList The list of files in this change and
+     *     patch range.
+     * @param {number} direction Either 1 (next file) or -1 (prev file).
+     * @param {(number|boolean)} opt_noUp Whether to return to the change view
+     *     when advancing the file goes outside the bounds of fileList.
+     *
+     * @return {?string} The next URL when proceeding in the specified
+     *     direction.
+     */
+    _computeNavLinkURL: function(path, fileList, direction, opt_noUp) {
+      if (!path || fileList.length === 0) { return null; }
+
+      var idx = fileList.indexOf(path);
+      if (idx === -1) { return null; }
+
+      idx += direction;
+      // Redirect to the change view if opt_noUp isn’t truthy and idx falls
+      // outside the bounds of [0, fileList.length).
       if (idx < 0 || idx > fileList.length - 1) {
-        page.show(this._getChangePath(
+        if (opt_noUp) { return null; }
+        return this._getChangePath(
             this._changeNum,
             this._patchRange,
-            this._change && this._change.revisions));
-        return;
+            this._change && this._change.revisions);
       }
-      page.show(this._getDiffURL(this._changeNum,
-                                 this._patchRange,
-                                 fileList[idx]));
+      return this._getDiffURL(this._changeNum, this._patchRange, fileList[idx]);
     },
 
     _paramsChanged: function(value) {
@@ -484,5 +509,9 @@
       this.$.cursor.moveToLineNumber(detail.number, detail.side);
       history.pushState(null, null, '#' + this.$.cursor.getAddress());
     },
+
+    _handleDropdownChange: function(e) {
+      e.target.blur();
+    },
   });
 })();
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 0a4d6b6..979ed55 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
@@ -32,6 +32,12 @@
   </template>
 </test-fixture>
 
+<test-fixture id="blank">
+  <template>
+    <div></div>
+  </template>
+</text-fixture>
+
 <script>
   suite('gr-diff-view tests', function() {
     var element;
@@ -338,6 +344,52 @@
       assert.equal(linkEls[2].getAttribute('href'), '/c/42/5..10/wheatley.md');
     });
 
+    test('prev/next links', function() {
+      element._changeNum = '42';
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '10',
+      };
+      element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+      element._path = 'glados.txt';
+      flushAsynchronousOperations();
+      var linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
+      assert.equal(linkEls.length, 2);
+      assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/chell.go');
+      assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/wheatley.md');
+      element._path = 'wheatley.md';
+      flushAsynchronousOperations();
+      assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/glados.txt');
+      assert.isFalse(linkEls[1].hasAttribute('href'));
+      element._path = 'chell.go';
+      flushAsynchronousOperations();
+      assert.isFalse(linkEls[0].hasAttribute('href'));
+      assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/glados.txt');
+    });
+
+    test('prev/next links with patch range', function() {
+      element._changeNum = '42';
+      element._patchRange = {
+        basePatchNum: '5',
+        patchNum: '10',
+      };
+      element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+      element._path = 'glados.txt';
+      flushAsynchronousOperations();
+      var linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
+      assert.equal(linkEls.length, 2);
+      assert.equal(linkEls[0].getAttribute('href'), '/c/42/5..10/chell.go');
+      assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10/wheatley.md');
+      element._path = 'wheatley.md';
+      flushAsynchronousOperations();
+      assert.equal(linkEls[0].getAttribute('href'), '/c/42/5..10/glados.txt');
+      assert.isFalse(linkEls[1].hasAttribute('href'));
+      element._path = 'chell.go';
+      flushAsynchronousOperations();
+      assert.isFalse(linkEls[0].hasAttribute('href'));
+      assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10/glados.txt');
+    });
+
     test('file review status', function(done) {
       element._loggedIn = true;
       element._changeNum = '42';
@@ -371,7 +423,7 @@
     test('diff mode selector correctly toggles the diff', function() {
       var select = element.$.modeSelect;
       var diffDisplay = element.$.diff;
-
+      var blurSpy = sinon.spy(select, 'blur');
       element._userPrefs = {diff_view: 'SIDE_BY_SIDE'};
 
       // The mode selected in the view state reflects the selected option.
@@ -392,6 +444,30 @@
       assert.equal(element._getDiffViewMode(), newMode);
       assert.equal(element._getDiffViewMode(), select.value);
       assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
+      assert(blurSpy.called, 'select should be blurred after selection');
+    });
+
+    test('diff mode selector initializes from preferences', function() {
+      var resolvePrefs;
+      var prefsPromise = new Promise(function(resolve) {
+        resolvePrefs = resolve;
+      });
+      var getPreferencesStub = sinon.stub(element.$.restAPI, 'getPreferences',
+          function() { return prefsPromise; });
+
+      // Attach a new gr-diff-view so we can intercept the preferences fetch.
+      var view = document.createElement('gr-diff-view');
+      var select = view.$.modeSelect;
+      fixture('blank').appendChild(view);
+      flushAsynchronousOperations();
+
+      // At this point the diff mode doesn't yet have the user's preference.
+      assert.equal(select.value, 'SIDE_BY_SIDE');
+
+      // Receive the overriding preference.
+      resolvePrefs({diff_view: 'UNIFIED'});
+      flushAsynchronousOperations();
+      assert.equal(select.value, 'SIDE_BY_SIDE');
     });
 
     test('_loadHash', function() {
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 46612a0..4672b31 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -91,13 +91,6 @@
         vertical-align: top;
         white-space: pre;
       }
-      .contentText:empty:before {
-        /**
-         * Insert glyph to prevent empty diff content from collapsing.
-         * "\200B" is a 'ZERO WIDTH SPACE' (U+200B)
-         */
-        content: "\200B";
-      }
       .contextLineNum:before,
       .lineNum:before {
         display: inline-block;
@@ -152,17 +145,17 @@
       }
       .tab {
         display: inline-block;
-        position: relative;
       }
-      .tab.withIndicator {
-        color: #D68E47;
-        text-decoration: line-through;
+      .tab-indicator:before {
+        color: #C62828;
+        /* >> character */
+        content: '\00BB';
       }
     </style>
     <style include="gr-theme-default"></style>
     <div class$="[[_computeContainerClass(_loggedIn, viewMode)]]"
         on-tap="_handleTap">
-      <gr-diff-selection>
+      <gr-diff-selection diff="[[_diff]]">
         <gr-diff-highlight
             id="highlights"
             logged-in="[[_loggedIn]]"
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 dbcbb38..7b93086 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -34,6 +34,10 @@
 
     properties: {
       changeNum: String,
+      hidden: {
+        type: Boolean,
+        observer: '_handleShowDiff',
+      },
       patchRange: Object,
       path: String,
       prefs: {
@@ -86,6 +90,12 @@
       }.bind(this));
     },
 
+    _handleShowDiff: function(hidden) {
+      if (!hidden) {
+        this.reload();
+      }
+    },
+
     reload: function() {
       this._clearDiffContent();
 
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 c33eadb..2f0b6bd 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
@@ -467,6 +467,17 @@
           assert.equal(drafts.length, 1);
           assert.equal(drafts[0].id, id);
         });
+
+        test('_handleShowDiff reloads when hidden is made false',
+            function(done) {
+          var stub = sinon.stub(element, 'reload', function() {
+            assert.isTrue(stub.called);
+            stub.restore();
+            done();
+          });
+          var spy = sinon.spy(element, '_handleShowDiff');
+          element.set('hidden', false);
+        });
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index 24d36c4..b50043e 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -33,6 +33,7 @@
         rangeStr = leftPatch + '..' + rangeStr;
       }
       page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path);
+      e.target.blur();
     },
 
     _computeLeftSelected: function(patchNum, patchRange) {
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index c7e1196..95789bc 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -63,6 +63,7 @@
       var showStub = sinon.stub(page, 'show');
       var leftSelectEl = element.$.leftPatchSelect;
       var rightSelectEl = element.$.rightPatchSelect;
+      var blurSpy = sinon.spy(leftSelectEl, 'blur');
       element.changeNum = '42';
       element.path = 'path/to/file.txt';
       element.availablePatches = ['1', '2', '3'];
@@ -77,6 +78,7 @@
               'Should navigate to /c/42/3/path/to/file.txt');
           leftSelectEl.value = '1';
           element.fire('change', {}, {node: leftSelectEl});
+          assert(blurSpy.called, 'Dropdown should be blurred after selection');
         } else if (numEvents == 2) {
           assert(showStub.lastCall.calledWithExactly(
               '/c/42/1..3/path/to/file.txt'),
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
index c5c9377..9c5d6bf 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
@@ -14,7 +14,12 @@
 limitations under the License.
 -->
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../gr-syntax-lib-loader/gr-syntax-lib-loader.html">
+
 <dom-module id="gr-syntax-layer">
+  <template>
+    <gr-syntax-lib-loader id="libLoader"></gr-syntax-lib-loader>
+  </template>
   <script src="../gr-diff/gr-diff-line.js"></script>
   <script src="../gr-diff-highlight/gr-annotation.js"></script>
   <script src="gr-syntax-layer.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index 478bcc8..c0ee1fa 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -106,6 +106,7 @@
         value: function() { return []; },
       },
       _processHandle: Number,
+      _hljs: Object,
     },
 
     addListener: function(fn) {
@@ -278,7 +279,6 @@
     _processNextLine: function(state) {
       var baseLine = undefined;
       var revisionLine = undefined;
-      var hljs = this._getHighlightLib();
 
       var section = this.diff.content[state.sectionIndex];
       if (section.ab) {
@@ -301,15 +301,15 @@
       var result;
 
       if (this._baseLanguage && baseLine !== undefined) {
-        result = hljs.highlight(this._baseLanguage, baseLine, true,
+        result = this._hljs.highlight(this._baseLanguage, baseLine, true,
             state.baseContext);
         this.push('_baseRanges', this._rangesFromString(result.value));
         state.baseContext = result.top;
       }
 
       if (this._revisionLanguage && revisionLine !== undefined) {
-        result = hljs.highlight(this._revisionLanguage, revisionLine, true,
-            state.revisionContext);
+        result = this._hljs.highlight(this._revisionLanguage, revisionLine,
+            true, state.revisionContext);
         this.push('_revisionRanges', this._rangesFromString(result.value));
         state.revisionContext = result.top;
       }
@@ -358,45 +358,10 @@
       });
     },
 
-    _getHighlightLib: function() {
-      return window.hljs;
-    },
-
-    _isHighlightLibLoaded: function() {
-      return !!this._getHighlightLib();
-    },
-
-    _configureHighlightLib: function() {
-      this._getHighlightLib().configure(
-          {classPrefix: 'gr-diff gr-syntax gr-syntax-'});
-    },
-
-    _getLibRoot: function() {
-      if (this._cachedLibRoot) { return this._cachedLibRoot; }
-
-      return this._cachedLibRoot = document.head
-          .querySelector('link[rel=import][href$="gr-app.html"]')
-          .href
-          .match(/(.+\/)elements\/gr-app\.html/)[1];
-    },
-    _cachedLibRoot: null,
-
-    /**
-     * Load and configure the HighlightJS library. If the library is already
-     * loaded, then do nothing and resolve.
-     * @return {Promise}
-     */
     _loadHLJS: function() {
-      if (this._isHighlightLibLoaded()) { return Promise.resolve(); }
-      return new Promise(function(resolve) {
-        var script = document.createElement('script');
-        script.src = this._getLibRoot() + HLJS_PATH;
-        script.onload = function() {
-          this._configureHighlightLib();
-          resolve();
-        }.bind(this);
-        Polymer.dom(this.root).appendChild(script);
+      return this.$.libLoader.get().then(function(hljs) {
+        this._hljs = hljs;
       }.bind(this));
-    }
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 5106671..178f61b 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -182,8 +182,8 @@
 
       var mockHLJS = getMockHLJS();
       var highlightSpy = sinon.spy(mockHLJS, 'highlight');
-      sandbox.stub(element, '_getHighlightLib',
-          function() { return mockHLJS; });
+      sandbox.stub(element.$.libLoader, 'get',
+          function() { return Promise.resolve(mockHLJS); });
       var processNextSpy = sandbox.spy(element, '_processNextLine');
       var processPromise = element.process();
 
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html
new file mode 100644
index 0000000..fedd22a
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.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">
+
+<dom-module id="gr-syntax-lib-loader">
+  <script src="gr-syntax-lib-loader.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js
new file mode 100644
index 0000000..520f24d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js
@@ -0,0 +1,93 @@
+// 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 HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
+  var LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/;
+
+  Polymer({
+    is: 'gr-syntax-lib-loader',
+
+    properties: {
+      _state: {
+        type: Object,
+
+        // NOTE: intended singleton.
+        value: {
+          loaded: false,
+          loading: false,
+          callbacks: [],
+        },
+      }
+    },
+
+    get: function() {
+      return new Promise(function(resolve) {
+        // If the lib is totally loaded, resolve immediately.
+        if (this._state.loaded) {
+          resolve(this._getHighlightLib());
+          return;
+        }
+
+        // If the library is not currently being loaded, then start loading it.
+        if (!this._state.loading) {
+          this._state.loading = true;
+          this._loadHLJS().then(this._onLibLoaded.bind(this));
+        }
+
+        this._state.callbacks.push(resolve);
+      }.bind(this));
+    },
+
+    _onLibLoaded: function() {
+      var lib = this._getHighlightLib();
+      this._state.loaded = true;
+      this._state.loading = false;
+      this._state.callbacks.forEach(function(cb) { cb(lib); });
+      this._state.callbacks = [];
+    },
+
+    _getHighlightLib: function() {
+      return window.hljs;
+    },
+
+    _configureHighlightLib: function() {
+      this._getHighlightLib().configure(
+          {classPrefix: 'gr-diff gr-syntax gr-syntax-'});
+    },
+
+    _getLibRoot: function() {
+      if (this._cachedLibRoot) { return this._cachedLibRoot; }
+
+      return this._cachedLibRoot = document.head
+          .querySelector('link[rel=import][href$="gr-app.html"]')
+          .href
+          .match(LIB_ROOT_PATTERN)[1];
+    },
+    _cachedLibRoot: null,
+
+    _loadHLJS: function() {
+      return new Promise(function(resolve) {
+        var script = document.createElement('script');
+        script.src = this._getLibRoot() + HLJS_PATH;
+        script.onload = function() {
+          this._configureHighlightLib();
+          resolve();
+        }.bind(this);
+        Polymer.dom(document.head).appendChild(script);
+      }.bind(this));
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
new file mode 100644
index 0000000..13bea04
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
@@ -0,0 +1,95 @@
+<!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-syntax-lib-loader</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-syntax-lib-loader.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-syntax-lib-loader></gr-syntax-lib-loader>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-syntax-lib-loader tests', function() {
+    var element;
+    var resolveLoad;
+    var loadStub;
+
+    setup(function() {
+      element = fixture('basic');
+
+      loadStub = sinon.stub(element, '_loadHLJS', function() {
+        return new Promise(function(resolve) {
+          resolveLoad = resolve;
+        });
+      });
+
+      // Assert preconditions:
+      assert.isFalse(element._state.loaded);
+      assert.isFalse(element._state.loading);
+    });
+
+    teardown(function() {
+      if (window.hljs) {
+        delete window.hljs;
+      }
+      loadStub.restore();
+
+      // Because the element state is a singleton, clean it up.
+      element._state.loading = false;
+      element._state.loaded = false;
+      element._state.callbacks = [];
+    });
+
+    test('only load once', function(done) {
+      var firstCallHandler = sinon.stub();
+      element.get().then(firstCallHandler);
+
+      // It should now be in the loading state.
+      assert.isTrue(loadStub.called);
+      assert.isTrue(element._state.loading);
+      assert.isFalse(element._state.loaded);
+      assert.isFalse(firstCallHandler.called);
+
+      var secondCallHandler = sinon.stub();
+      element.get().then(secondCallHandler);
+
+      // No change in state.
+      assert.isTrue(element._state.loading);
+      assert.isFalse(element._state.loaded);
+      assert.isFalse(firstCallHandler.called);
+      assert.isFalse(secondCallHandler.called);
+
+      // Now load the library.
+      resolveLoad();
+      flush(function() {
+        // The state should be loaded and both handlers called.
+        assert.isFalse(element._state.loading);
+        assert.isTrue(element._state.loaded);
+        assert.isTrue(firstCallHandler.called);
+        assert.isTrue(secondCallHandler.called);
+        done();
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index c20795b..25dfaff 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -22,6 +22,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="./core/gr-reporting/gr-reporting.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">
@@ -136,6 +137,7 @@
     </gr-overlay>
     <gr-error-manager></gr-error-manager>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="gr-app.js"></script>
 </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 0833a72..f24ecfd 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -72,6 +72,7 @@
     },
 
     ready: function() {
+      this.$.reporting.appStarted();
       this._viewState = {
         changeView: {
           changeNum: null,
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
new file mode 100644
index 0000000..0c23c4f
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -0,0 +1,53 @@
+<!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-app</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-app.html">
+
+<test-fixture id="basic">
+  <template>
+    <gr-app id="app"></gr-app>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-app tests', function() {
+    var sandbox;
+    var element;
+
+    setup(function(done) {
+      sandbox = sinon.sandbox.create();
+      stub('gr-reporting', {
+        appStarted: sandbox.stub(),
+      });
+      element = fixture('basic');
+      flush(done);
+    });
+    teardown(function() {
+      sandbox.restore();
+    });
+
+    test('reporting', function() {
+      assert.isTrue(element.$.reporting.appStarted.calledOnce);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 0fc6b07..60c7f25 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,6 +14,8 @@
 (function() {
   'use strict';
 
+  var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
+
   Polymer({
     is: 'gr-autocomplete',
 
@@ -181,6 +183,12 @@
           this._cancel();
           break;
         case 9: // Tab
+          if (this._suggestions.length > 0) {
+            e.preventDefault();
+            this._commit();
+            this._suggestions = [];
+          }
+          break;
         case 13: // Enter
           e.preventDefault();
           this._commit();
@@ -199,8 +207,10 @@
       var completed = suggestions[index].value;
       if (this.multi) {
         // Append the completed text to the end of the string.
-        var shortStr = this.text.substring(0, this.text.lastIndexOf(' ') + 1);
-        this.value = shortStr + completed;
+        // Allow spaces within quoted terms.
+        var tokens = this.text.match(TOKENIZE_REGEX);
+        tokens[tokens.length - 1] = completed;
+        this.value = tokens.join(' ');
       } else {
         this.value = completed;
       }
@@ -226,7 +236,7 @@
       if (this._suggestions.length > 0) {
         this._updateValue(this._suggestions, this._index);
       } else {
-        this.value = this.text;
+        this.value = this.text || '';
       }
 
       var value = this.value;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index f8b16b7..ccc578c 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -241,5 +241,16 @@
         done();
       });
     });
+
+    test('tab key completes only when suggestions exist', function() {
+      var commitStub = sinon.stub(element, '_commit');
+      element._suggestions = [];
+      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab
+      assert.isFalse(commitStub.called);
+      element._suggestions = ['tunnel snakes rule!'];
+      MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab
+      assert.isTrue(commitStub.called);
+      commitStub.restore();
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index c815ffd..164bb2d 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -22,7 +22,7 @@
   <template strip-whitespace>
     <style>
       :host {
-        background-color: #fff;
+        background-color: #f5f5f5;
         border: 1px solid #d1d2d3;
         border-radius: 2px;
         box-sizing: border-box;
@@ -30,10 +30,10 @@
         cursor: pointer;
         display: inline-block;
         font-family: var(--font-family);
-        font-size: 13px;
+        font-size: 12px;
         font-weight: bold;
         outline-width: 0;
-        padding: .3em .65em;
+        padding: .4em .85em;
         position: relative;
         text-align: center;
         -moz-user-select: none;
@@ -44,10 +44,17 @@
       :host([hidden]) {
         display: none;
       }
+      :host([primary]),
+      :host([secondary]) {
+        color: #fff;
+      }
       :host([primary]) {
         background-color: #4d90fe;
         border-color: #3079ed;
-        color: #fff;
+      }
+      :host([secondary]) {
+        background-color: #d14836;
+        border-color: transparent;
       }
       :host([small]) {
         font-size: 12px;
@@ -74,32 +81,53 @@
       :host([loading][disabled]) {
         cursor: wait;
       }
-      :host(:focus),
-      :host(:hover) {
-        border-color: #666;
+      :host(:focus:not([primary]:not[secondary])),
+      :host(:hover:not([primary]:not[secondary])) {
+        background-color: #f8f8f8;
+        border-color: #aaa;
       }
       :host(:active) {
         border-color: #d1d2d3;
         color: #aaa;
       }
+      :host([primary]:focus),
+      :host([secondary]:focus),
+      :host([primary]:active),
+      :host([secondary]:active) {
+        color: #fff;
+      }
       :host([primary]:focus) {
-        border-color: #fff;
         box-shadow: 0 0 1px #00f;
       }
       :host([primary]:hover) {
+        background-color: #4d90fe;
         border-color: #00F;
       }
+      :host([primary]:active),
+      :host([secondary]:active) {
+        box-shadow: none;
+      }
       :host([primary]:active) {
         border-color: #0c2188;
-        box-shadow: none;
-        color: #fff;
       }
-      :host([primary][loading]),
-      :host([primary][disabled]) {
+      :host([secondary]:focus) {
+        box-shadow: 0 0 1px #f00;
+      }
+      :host([secondary]:hover) {
+        background-color: #c53727;
+        border: 1px solid #b0281a;
+      }
+      :host([secondary]:active) {
+        border-color: #941c0c;
+      }
+      :host([primary][loading]) {
         background-color: #7caeff;
         border-color: transparent;
         color: #fff;
       }
+      :host([primary][disabled]) {
+        background-color: #888;
+      }
     </style>
     <content></content>
   </template>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 0d3ea3d..7b3bc23 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -63,15 +63,6 @@
         type: String,
         value: ScrollBehavior.NEVER,
       },
-
-      /**
-       * When using the 'keep-visible' scroll behavior, set an offset to the top
-       * of the window for what is considered above the upper fold.
-       */
-      foldOffsetTop: {
-        type: Number,
-        value: 0,
-      },
     },
 
     detached: function() {
@@ -214,7 +205,7 @@
       }
 
       if (this.scroll === ScrollBehavior.KEEP_VISIBLE &&
-          top > window.pageYOffset + this.foldOffsetTop &&
+          top > window.pageYOffset &&
           top < window.pageYOffset + window.innerHeight) { return; }
 
       // Scroll the element to the middle of the window. Dividing by a third
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 2f109c9..632ba06 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
@@ -426,7 +426,7 @@
 
       var bLastDotIndex = b.lastIndexOf('.');
       var bExt = b.substr(bLastDotIndex + 1);
-      var bFile = a.substr(0, bLastDotIndex);
+      var bFile = b.substr(0, bLastDotIndex);
 
       // Sort header files above others with the same base name.
       var headerExts = ['h', 'hxx', 'hpp'];
@@ -442,8 +442,7 @@
           return 1;
         }
       }
-
-      return a.localeCompare(b);
+      return aFile.localeCompare(bFile) || a.localeCompare(b);
     },
 
     getChangeRevisionActions: function(changeNum, patchNum) {
@@ -467,8 +466,26 @@
       });
     },
 
-    getSuggestedProjects: function(inputVal, opt_errFn, opt_ctx) {
-      return this.fetchJSON('/projects/', opt_errFn, opt_ctx, {p: inputVal});
+    getSuggestedGroups: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+      return this.fetchJSON('/groups/', opt_errFn, opt_ctx, {
+        s: inputVal,
+        n: opt_n,
+      });
+    },
+
+    getSuggestedProjects: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+      return this.fetchJSON('/projects/', opt_errFn, opt_ctx, {
+        p: inputVal,
+        n: opt_n,
+      });
+    },
+
+    getSuggestedAccounts: function(inputVal, opt_n, opt_errFn, opt_ctx) {
+      return this.fetchJSON('/accounts/', opt_errFn, opt_ctx, {
+        q: inputVal,
+        n: opt_n,
+        suggest: null,
+      });
     },
 
     addChangeReviewer: function(changeNum, reviewerID) {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 8dda2ce..8f994f0 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -237,6 +237,32 @@
           ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort(
               element._specialFilePathCompare),
           ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']);
+
+      // Regression test for Issue 4448.
+      assert.deepEqual([
+          'minidump/minidump_memory_writer.cc',
+          'minidump/minidump_memory_writer.h',
+          'minidump/minidump_thread_writer.cc',
+          'minidump/minidump_thread_writer.h',
+          ]
+        .sort(element._specialFilePathCompare),
+          [
+            'minidump/minidump_memory_writer.h',
+            'minidump/minidump_memory_writer.cc',
+            'minidump/minidump_thread_writer.h',
+            'minidump/minidump_thread_writer.cc',
+          ]);
+
+      // Regression test for Issue 4545.
+      assert.deepEqual([
+          'task_test.go',
+          'task.go',
+          ]
+        .sort(element._specialFilePathCompare),
+          [
+            'task.go',
+            'task_test.go',
+          ]);
     });
 
     test('rebase always enabled', function(done) {
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index ecf4ac6..faf45d8 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -20,7 +20,7 @@
   --selection-background-color: #ebf5fb;
   --default-text-color: #000;
   --view-background-color: #fff;
-  --default-horizontal-margin: 1.25rem;
+  --default-horizontal-margin: 1rem;
   --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
   --monospace-font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace;
 
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index d3cb316..156c873 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -25,15 +25,15 @@
   var basePath = '../elements/';
 
   [
-    'change-list/gr-change-list-item/gr-change-list-item_test.html',
-    'change-list/gr-change-list/gr-change-list_test.html',
     'change/gr-account-entry/gr-account-entry_test.html',
     'change/gr-account-list/gr-account-list_test.html',
     'change/gr-change-actions/gr-change-actions_test.html',
     'change/gr-change-metadata/gr-change-metadata_test.html',
     'change/gr-change-view/gr-change-view_test.html',
     'change/gr-comment-list/gr-comment-list_test.html',
+    'change/gr-commit-info/gr-commit-info_test.html',
     'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
+    'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html',
     'change/gr-download-dialog/gr-download-dialog_test.html',
     'change/gr-file-list/gr-file-list_test.html',
     'change/gr-message/gr-message_test.html',
@@ -41,26 +41,31 @@
     'change/gr-related-changes-list/gr-related-changes-list_test.html',
     'change/gr-reply-dialog/gr-reply-dialog_test.html',
     'change/gr-reviewer-list/gr-reviewer-list_test.html',
+    'change-list/gr-change-list/gr-change-list_test.html',
+    'change-list/gr-change-list-item/gr-change-list-item_test.html',
     'core/gr-account-dropdown/gr-account-dropdown_test.html',
     'core/gr-error-manager/gr-error-manager_test.html',
     'core/gr-main-header/gr-main-header_test.html',
+    'core/gr-reporting/gr-reporting_test.html',
     'core/gr-search-bar/gr-search-bar_test.html',
+    'diff/gr-diff/gr-diff-group_test.html',
+    'diff/gr-diff/gr-diff_test.html',
     'diff/gr-diff-builder/gr-diff-builder_test.html',
-    'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
     'diff/gr-diff-comment/gr-diff-comment_test.html',
+    'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
     'diff/gr-diff-cursor/gr-diff-cursor_test.html',
-    'diff/gr-diff-highlight/gr-diff-highlight_test.html',
     'diff/gr-diff-highlight/gr-annotation_test.html',
+    'diff/gr-diff-highlight/gr-diff-highlight_test.html',
     'diff/gr-diff-preferences/gr-diff-preferences_test.html',
     'diff/gr-diff-processor/gr-diff-processor_test.html',
     'diff/gr-diff-selection/gr-diff-selection_test.html',
     'diff/gr-diff-view/gr-diff-view_test.html',
-    'diff/gr-diff/gr-diff-group_test.html',
-    'diff/gr-diff/gr-diff_test.html',
     'diff/gr-patch-range-select/gr-patch-range-select_test.html',
     'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
     'diff/gr-selection-action-box/gr-selection-action-box_test.html',
     'diff/gr-syntax-layer/gr-syntax-layer_test.html',
+    'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html',
+    'gr-app_test.html',
     'settings/gr-account-info/gr-account-info_test.html',
     'settings/gr-email-editor/gr-email-editor_test.html',
     'settings/gr-group-list/gr-group-list_test.html',
@@ -69,10 +74,10 @@
     'settings/gr-settings-view/gr-settings-view_test.html',
     'settings/gr-ssh-editor/gr-ssh-editor_test.html',
     'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
-    'shared/gr-autocomplete/gr-autocomplete_test.html',
     'shared/gr-account-label/gr-account-label_test.html',
     'shared/gr-account-link/gr-account-link_test.html',
     'shared/gr-alert/gr-alert_test.html',
+    'shared/gr-autocomplete/gr-autocomplete_test.html',
     'shared/gr-avatar/gr-avatar_test.html',
     'shared/gr-change-star/gr-change-star_test.html',
     'shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
diff --git a/tools/java_doc.defs b/tools/java_doc.defs
index 41a8730..2bf27cf 100644
--- a/tools/java_doc.defs
+++ b/tools/java_doc.defs
@@ -2,22 +2,20 @@
     name,
     title,
     pkgs,
-    paths,
+    source_jar,
     srcs = [],
     deps = [],
     visibility = [],
-    do_it_wrong = False,
     external_docs = [],
   ):
-  if do_it_wrong:
-    sourcepath = paths
-  else:
-    sourcepath = ['$SRCDIR/' + n for n in paths]
+  # TODO(davido): Actually we shouldn't need to extract the source
+  # archive, javadoc should just work with provided archive.
   external_docs.insert(0, 'http://docs.oracle.com/javase/7/docs/api')
   genrule(
     name = name,
     cmd = ' '.join([
-      'while ! test -f .buckconfig; do cd ..; done;',
+      'mkdir $TMP/sourcepath &&',
+      'unzip $(location %s) -d $TMP/sourcepath &&' % source_jar,
       'javadoc',
       '-quiet',
       '-protected',
@@ -28,8 +26,7 @@
       ' '.join(['-link %s' % url for url in external_docs]),
       '-subpackages ',
       ':'.join(pkgs),
-      '-sourcepath ',
-      ':'.join(sourcepath),
+      '-sourcepath $TMP/sourcepath',
       ' -classpath ',
       ':'.join(['$(classpath %s)' % n for n in deps]),
       '-d $TMP',
@@ -37,4 +34,4 @@
     srcs = srcs,
     out = name + '.jar',
     visibility = visibility,
-)
+  )
diff --git a/tools/jgit-snapshot-deploy-pom.diff b/tools/jgit-snapshot-deploy-pom.diff
new file mode 100644
index 0000000..01f50e4
--- /dev/null
+++ b/tools/jgit-snapshot-deploy-pom.diff
@@ -0,0 +1,43 @@
+diff --git a/pom.xml b/pom.xml
+index d256bbb..7e523fd 100644
+--- a/pom.xml
++++ b/pom.xml
+@@ -226,6 +226,10 @@
+ 
+   <pluginRepositories>
+     <pluginRepository>
++      <id>gerrit-maven</id>
++      <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
++    </pluginRepository>
++    <pluginRepository>
+       <id>repo.eclipse.org.cbi-releases</id>
+       <url>https://repo.eclipse.org/content/repositories/cbi-releases/</url>
+     </pluginRepository>
+@@ -236,6 +240,13 @@
+   </pluginRepositories>
+ 
+   <build>
++    <extensions>
++      <extension>
++        <groupId>com.googlesource.gerrit</groupId>
++        <artifactId>gs-maven-wagon</artifactId>
++        <version>3.3</version>
++      </extension>
++    </extensions>
+     <pluginManagement>
+       <plugins>
+         <plugin>
+@@ -649,9 +660,10 @@
+ 
+   <distributionManagement>
+     <repository>
+-      <id>repo.eclipse.org</id>
+-      <name>JGit Maven Repository - Releases</name>
+-      <url>https://repo.eclipse.org/content/repositories/jgit-releases/</url>
++      <id>gerrit-maven-repository</id>
++      <name>Gerrit Maven Repository</name>
++      <url>gs://gerrit-maven</url>
++      <uniqueVersion>true</uniqueVersion>
+     </repository>
+     <snapshotRepository>
+       <id>repo.eclipse.org</id>