Merge "Save branch tip after resolved from ref database"
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 51ea9c5..63a4344 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -1,163 +1,160 @@
 = Gerrit Code Review - Mail Templates
 
-Gerrit uses velocity templates for the bulk of the standard mails it sends out.
+Gerrit uses Closure Templates for the bulk of the standard mails it sends out.
 There are builtin default templates which are used if they are not overridden.
 These defaults are also provided as examples so that administrators may copy
 them and easily modify them to tweak their contents.
 
+*Compatibility Note:* previously, Velocity Template Language (VTL) was used as
+the template language for Gerrit emails. VTL has now been deprecated in favor of
+Soy, but Velocity templates that modify text emails remain supported for now.
 
 == Template Locations and Extensions:
 
 The default example templates reside under:  `'$site_path'/etc/mail` and are
-terminated with the double extension `.vm.example`. Modifying these example
+terminated with the double extension `.soy.example`. Modifying these example
 files will have no effect on the behavior of Gerrit.  However, copying an
 example template to an equivalently named file without the `.example` extension
 and modifying it will allow an administrator to customize the template.
 
-
 == Supported Mail Templates:
 
 Each mail that Gerrit sends out is controlled by at least one template.  These
 are listed below.  Change emails are influenced by two additional templates,
 one to set the subject line, and one to set the footer which gets appended to
-all the change emails (see `ChangeSubject.vm` and `ChangeFooter.vm` below.)
+all the change emails (see `ChangeSubject.soy` and `ChangeFooter.soy` below.)
 
-=== Abandoned.vm
+Many types of Gerrit email message support HTML in addition to plain-text. Where
+both are supported, templates to control the HTML part have `...Html` appended
+in their file names. For example, for "Abandoned" emails, the `Abandoned.soy`
+template determines the text part of the message, whereas `AbandonedHtml.soy`
+determines the HTML part.
 
-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`.
+=== Abandoned.soy and AbandonedHtml.soy
 
-=== AddKey.vm
+The "Abandoned" templates will determine the contents of the email related to a
+change being abandoned.  It is a `ChangeEmail`: see `ChangeSubject.soy` and
+ChangeFooter.
 
-The `AddKey.vm` template will determine the contents of the email related to
-SSH and GPG keys being added to a user account. This notification is not sent
-when the key is administratively added to another user account.
+=== AddKey.soy and AddKeyHtml.soy
 
-=== ChangeFooter.vm
+AddKey templates will determine the contents of the email related to SSH and GPG
+keys being added to a user account. This notification is not sent when the key
+is administratively added to another user account.
 
-The `ChangeFooter.vm` template will determine the contents of the footer
-text that will be appended to emails related to changes (all `ChangeEmail`s).
+=== ChangeFooter.soy and ChangeFooterHtml.soy
 
-=== ChangeSubject.vm
+The ChangeFooter templates will determine the contents of the footer that will
+be appended to emails related to changes (all `ChangeEmail`s).
 
-The `ChangeSubject.vm` template will determine the contents of the email
+=== ChangeSubject.soy
+
+The `ChangeSubject.soy` template will determine the contents of the email
 subject line for ALL emails related to changes.
 
-=== Comment.vm
+=== Comment.soy
 
-The `Comment.vm` template will determine the contents of the email related to
+The `Comment.soy` 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`.
+`ChangeSubject.soy`, ChangeFooter and CommentFooter.
 
-=== CommentFooter.vm
+=== CommentFooter.soy and CommentFooterHtml.soy
 
-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`, `Comment.vm` and `ChangeFooter.vm`.
+The CommentFooter templates will determine the contents of the footer text that
+will be appended to emails related to a user submitting comments on changes.
+See `ChangeSubject.soy`, Comment and ChangeFooter.
 
-=== DeleteVote.vm
+=== DeleteVote.soy and DeleteVoteHtml.soy
 
-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`.
+The DeleteVote templates will determine the contents of the email related to
+removing votes on changes.  It is a `ChangeEmail`: see `ChangeSubject.soy`
+and ChangeFooter.
 
-=== DeleteReviewer.vm
+=== DeleteReviewer.soy and DeleteReviewerHtml.soy
 
-The `DeleteReviewer.vm` template will determine the contents of the email related
-to a user removing a reviewer (with a vote) from a change.  It is a
-`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+The DeleteReviewer templates will determine the contents of the email related to
+a user removing a reviewer (with a vote) from a change.  It is a
+`ChangeEmail`: see `ChangeSubject.soy` and ChangeFooter.
 
-=== Footer.vm
+=== Footer.soy and FooterHtml.soy
 
-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.
+The Footer templates will determine the contents of the footer text appended to
+the end of all outgoing emails after the ChangeFooter and CommentFooter.
 
-=== Merged.vm
+=== Merged.soy and MergedHtml.soy
 
-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`.
+The Merged templates will determine the contents of the email related to a
+change successfully merged to the head.  It is a `ChangeEmail`: see
+`ChangeSubject.soy` and ChangeFooter.
 
-=== NewChange.vm
+=== NewChange.soy and NewChangeHtml.soy
 
-The `NewChange.vm` template will determine the contents of the email related
-to a user submitting a new change for review. This includes changes created
-by actions made by the user in the Web UI such as cherry picking a commit or
-reverting a change.  It is a `ChangeEmail`: see `ChangeSubject.vm` and
-`ChangeFooter.vm`.
+The NewChange templates will determine the contents of the email related to a
+user submitting a new change for review. This includes changes created by
+actions made by the user in the Web UI such as cherry picking a commit or
+reverting a change.  It is a `ChangeEmail`: see `ChangeSubject.soy` and
+ChangeFooter.
 
-=== RegisterNewEmail.vm
+=== RegisterNewEmail.soy
 
-The `RegisterNewEmail.vm` template will determine the contents of the email
+The `RegisterNewEmail.soy` template will determine the contents of the email
 related to registering new email accounts.
 
-=== ReplacePatchSet.vm
+=== ReplacePatchSet.soy and ReplacePatchSetHtml.soy
 
-The `ReplacePatchSet.vm` template will determine the contents of the email
-related to a user submitting a new patchset for a change.  This includes
-patchsets created by actions made by the user in the Web UI such as editing
-the commit message, cherry picking a commit, or rebasing a change.  It is a
-`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+The ReplacePatchSet templates will determine the contents of the email related
+to a user submitting a new patchset for a change.  This includes patchsets
+created by actions made by the user in the Web UI such as editing the commit
+message, cherry picking a commit, or rebasing a change.  It is a `ChangeEmail`:
+see `ChangeSubject.soy` and ChangeFooter.
 
-=== Restored.vm
+=== Restored.soy and RestoredHtml.soy
 
-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`.
+The Restored templates will determine the contents of the email related to a
+change being restored.  It is a `ChangeEmail`: see `ChangeSubject.soy` and
+ChangeFooter.
 
-=== Reverted.vm
+=== Reverted.soy and RevertedHtml.soy
 
-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`.
+The Reverted templates will determine the contents of the email related to a
+change being reverted.  It is a `ChangeEmail`: see `ChangeSubject.soy` and
+ChangeFooter.
 
 
 == Mail Variables and Methods
 
 Mail templates can access and display objects currently made available to them
-via the velocity context.  While the base objects are documented here, it is
-possible to call public methods on these objects from templates.  Those methods
-are not documented here since they could change with every release.  As these
-templates are meant to be modified only by a qualified sysadmin, it is accepted
-that writing templates for Gerrit emails is likely to require some basic
-knowledge of the class structure to be useful.  Browsing the source code might
-be necessary for anything more than a minor formatting change.
+via the Soy context.
 
 === Warning
 
 Be aware that modifying templates can cause them to fail to parse and therefore
-not send out the actual email, or worse, calling methods on the available
-objects could have internal side effects which would adversely affect the
-health of your Gerrit server and/or data.
+not send out the actual email.
 
 === All OutgoingEmails
 
 All outgoing emails have the following variables available to them:
 
-$email::
+$email.settingsUrl::
 +
-A reference to the class constructing the current `OutgoingEmail`.  With this
-reference it is possible to call any public method on the OutgoingEmail class
-or the current child class inherited from it.
+The URL to view the user's settings in the Gerrit web UI.
+
+$email.gerritHost::
++
+The name of the Gerrit instance.
+
+$email.gerritUrl::
++
+The URL to the Gerrit web UI.
 
 $messageClass::
 +
 A String containing the messageClass.
 
-$StringUtils::
-+
-A reference to the Apache `StringUtils` class.  This can be very useful for
-formatting strings.
-
 === Change Emails
 
-All change related emails have the following additional variables available to them:
-
-$change::
-+
-A reference to the current `Change` object.
+Change related emails have the following template data available to them, in
+addition to what's available to all outgoing emails.
 
 $changeId::
 +
@@ -167,30 +164,69 @@
 +
 The text of the `ChangeMessage`.
 
-$branch::
-+
-A reference to the branch of this change (a `Branch.NameKey`).
-
 $fromName::
 +
 The name of the from user.
 
+$email.unifiedDiff::
++
+The diff of the change.
+
+$email.changeDetail::
++
+The details of the change, including the commit message.
+
+$email.changeUrl::
++
+The URL to the change in the web UI.
+
+$email.includeDiff::
++
+Whether the Gerrit instance is configured to include diffs in emails.
+
+$change.subject::
++
+The subject of the current change.
+
+$change.originalSubject::
++
+The subject corresponding to the first patch set of the current change.
+
+$change.shortSubject::
++
+The subject limited to 63 characters, with an ellipsis if it exceeds that.
+
+$change.ownerEmail::
++
+The email address of the owner of the change.
+
+$branch.shortName::
++
+The name of the branch targeted by the current change.
+
 $projectName::
 +
 The name of this change's project.
 
-$patchSet::
+$shortProjectName::
 +
-A reference to the current `PatchSet`.
+The project name with the path abbreviated.
 
-$patchSetInfo::
+$sshHost::
 +
-A reference to the current `PatchSetInfo`.
+SSH hostname for the Gerrit instance.
 
+$patchSet.patchSetId::
++
+The current patch set number.
+
+$patchSet.refname::
++
+The refname of the patch set.
 
 == SEE ALSO
 
-* link:http://velocity.apache.org/[velocity]
+* link:https://developers.google.com/closure/templates/[Closure Templates]
 
 GERRIT
 ------
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 96695db..8741d80 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -83,6 +83,7 @@
 .. link:#push-stable[Push the Stable Branch]
 .. link:#push-tag[Push the Release Tag]
 .. link:#upload-documentation[Upload the Documentation]
+.. link:#finalize-release-notes[Finalize Release Notes]
 .. link:#update-issues[Update the Issues]
 .. link:#announce[Announce on Mailing List]
 . link:#increase-version[Increase Gerrit Version for Current Development]
@@ -327,15 +328,6 @@
 [[upload-documentation]]
 ==== Upload the Documentation
 
-* Build the release notes:
-+
-----
-  buck build releasenotes
-----
-
-* Extract the release notes files from the zip file generated from the previous
-step: `buck-out/gen/ReleaseNotes/html/html.zip`.
-
 * Extract the documentation files from the zip file generated from
 `buck build docs`: `buck-out/gen/Documentation/searchfree/searchfree.zip`.
 
@@ -344,6 +336,16 @@
 link:https://console.cloud.google.com/storage/browser/gerrit-documentation/?project=api-project-164060093628[
 gerrit-documentation] storage bucket.
 
+[[finalize-release-notes]]
+=== Finalize the Release Notes
+
+Upload a change on the homepage project to:
+
+* Remove 'In Development' caveat from the relevant section.
+
+* Add links to the released documentation and the .war file, and make the
+latest version bold.
+
 [[update-links]]
 ==== Update homepage links
 
@@ -370,7 +372,7 @@
 
 * Send an email to the mailing list to announce the release, consider
 including some or all of the following in the email:
-** A link to the release and the release notes (if a final release)
+** A link to the release and the release notes
 ** A link to the docs
 ** Describe the type of release (stable, bug fix, RC)
 ** Hash values (SHA1, SHA256, MD5) for the release WAR file.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 236d30e..e5073b2 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3472,7 +3472,7 @@
 for later processing by command line tools.
 
 [[submit-preview]]
-===Submit Preview
+=== Submit Preview
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/preview_submit'
 --
@@ -4195,7 +4195,7 @@
 
 .Request
 ----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/website%2Freleases%2Flogo.png/safe_content HTTP/1.0
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/website%2Freleases%2Flogo.png/download HTTP/1.0
 ----
 
 .Response
@@ -4209,7 +4209,7 @@
 
 .Request
 ----
-  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/safe_content?suffix=new HTTP/1.0
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/download?suffix=new HTTP/1.0
 ----
 
 .Response
@@ -5862,6 +5862,8 @@
 [[submit-record]]
 === SubmitRecord
 The `SubmitRecord` entity describes results from a submit_rule.
+Fields in this entity roughly correspond to the fields set by `LABELS`
+in link:#label-info[LabelInfo].
 
 [options="header",cols="1,^1,5"]
 |===========================
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 0b6de57..f1f1654 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -321,6 +321,20 @@
 +
 Same as <<status,status:'STATE'>>.
 
+is:submittable::
++
+True if the change is submittable according to the submit rules for
+the project, for example if all necessary labels have been voted on.
++
+This operator only takes into account one change at a time, not any
+related changes, and does not guarantee that the submit button will
+appear for matching changes. To check whether a submit button appears,
+use the
+link:rest-api-changes.html#get-revision-actions[Get Revision Actions]
+API.
++
+Equivalent to <<submittable,submittable:ok>>.
+
 [[mergeable]]
 is:mergeable::
 +
@@ -394,6 +408,15 @@
 'COMMITTER' may be the committer's exact email address, or part of the name or
 email address.
 
+[[submittable]]
+submittable:'SUBMIT_STATUS'::
++
+Changes having the given submit record status after applying submit
+rules. Valid statuses are in the `status` field of
+link:rest-api-changes.html#submit-record[SubmitRecord]. This operator
+only applies to the top-level status; individual label statuses can be
+searched link:#labels[by label].
+
 
 == Argument Quoting
 
@@ -448,8 +471,10 @@
   ('user=' or 'group=').  If an LDAP group is being referenced make
   sure to use 'ldap/<groupname>'.
 
-A label name must be followed by a score, or an operator and a score.
-The easiest way to explain this is by example.
+A label name must be followed by either a score with optional operator,
+or a label status. The easiest way to explain this is by example.
++
+First, some examples of scores with operators:
 
 `label:Code-Review=2`::
 `label:Code-Review=+2`::
@@ -473,8 +498,20 @@
 `label:Code-Review>=1`::
 +
 Matches changes with either a +1, +2, or any higher score.
++
+Instead of a numeric vote, you can provide a label status corresponding
+to one of the fields in the
+link:rest-api-changes.html#submit-record[SubmitRecord] REST API entity.
+
+`label:Non-Author-Code-Review=need`::
++
+Matches changes where the submit rules indicate that a label named
+`Non-Author-Code-Review` is needed. (See the
+link:prolog-cookbook.html#NonAuthorCodeReview[Prolog Cookbook] for how
+this label can be configured.)
 
 `label:Code-Review=+2,aname`::
+`label:Code-Review=ok,aname`::
 +
 Matches changes with a +2 code review where the reviewer or group is aname.
 
@@ -483,7 +520,9 @@
 Matches changes with a +2 code review where the reviewer is jsmith.
 
 `label:Code-Review=+2,user=owner`::
+`label:Code-Review=ok,user=owner`::
 `label:Code-Review=+2,owner`::
+`label:Code-Review=ok,owner`::
 +
 The special "owner" parameter corresponds to the change owner.  Matches
 all changes that have a +2 vote from the change owner.
@@ -498,10 +537,14 @@
 Matches changes with either a -1, -2, or any lower score.
 
 `is:open label:Code-Review+2 label:Verified+1 NOT label:Verified-1 NOT label:Code-Review-2`::
+`is:open label:Code-Review=ok label:Verified=ok`::
 +
-Matches changes that are ready to be submitted.
+Matches changes that are ready to be submitted according to one common
+label configuration. (For a more general check, use
+link:#submittable[submittable:ok].)
 
 `is:open (label:Verified-1 OR label:Code-Review-2)`::
+`is:open (label:Verified=reject OR label:Code-Review:reject)`::
 +
 Changes that are blocked from submission due to a blocking score.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.12.1.txt b/ReleaseNotes/ReleaseNotes-2.12.1.txt
index e746d6e..8f94810 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.1.txt
@@ -1,228 +1,5 @@
 = Release notes for Gerrit 2.12.1
 
-Gerrit 2.12.1 is now available:
-
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.1.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.12.1.war]
-
-Gerrit 2.12.1 includes the bug fixes done with
-link:ReleaseNotes-2.11.6.html[Gerrit 2.11.6] and
-link:ReleaseNotes-2.11.7.html[Gerrit 2.11.7]. These bug fixes are *not*
-listed in these release notes.
-
-== Schema Upgrade
-
-*WARNING:* This version includes a manual schema upgrade when upgrading
-from 2.12.
-
-When upgrading a site that is already running version 2.12, the `patch_sets`
-table must be manually migrated using the `gerrit gsql` SSH command or the
-`gqsl` site program.
-
-For the default H2 database, execute the command:
-
-----
-  alter table patch_sets modify push_certficate clob;
-----
-
-For MySQL, execute the command:
-
-----
-  alter table patch_sets modify push_certficate text;
-----
-
-For PostgreSQL, execute the command:
-
-----
-  alter table patch_sets alter column push_certficate type text;
-----
-
-For other database types, execute the appropriate equivalent command.
-
-Note that the misspelled `push_certficate` is the actual name of the
-column.
-
-When upgrading from a version earlier than 2.12, this manual step is not
-necessary and should be omitted.
-
-
-== Bug Fixes
-
-=== General
-
-* Fix column type for signed push certificates.
-+
-The column type `VARCHAR(255)` was too small, preventing some PGP push
-certificates from being stored.
-
-* Add the `DRAFT_COMMENTS` option to the list changes REST API endpoint
-and mark it as deprecated.
-+
-It was removed in version 2.12 because it's not needed any more by the UI,
-but this caused failures for clients that still use it.
-+
-Now it is added back, although it does not do anything and is marked as
-deprecated.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3669[Issue 3669]:
-Fix schema migration when migrating to 2.12.x directly from a version
-earlier than 2.11.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3733[Issue 3733]:
-Correctly detect symlinked log directory on startup.
-+
-If `$site_path/logs` was a symlink, the server would not start.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3871[Issue 3871]:
-Throw an explicit exception when failing to load a change from the database.
-+
-If a change could not be loaded from the database, for example if it was
-manually removed from the changes table but references to it were remaining
-in other tables, a null change was returned which would then lead to an
-'Internal Server Error' that was difficult to track down. Now an error is
-raised earlier which will help administrators to find the root cause.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3743[Issue 3743]:
-Use submitter identity as committer when using 'Rebase if Necessary' merge
-strategy.
-+
-When submitting a change that required rebase, the committer was being
-set to 'Gerrit Code Review' instead of the name of the submitter.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3758[Issue 3758]:
-Fix serving of static resources when deployed in application container.
-+
-When deployed in a container, for example Tomcat, it was not possible to
-load the UI because static content could not be loaded from the WAR file.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3790[Issue 3790]:
-When deployed in a container, for example Tomcat, the 'Documentation' menu
-was missing.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3786[Issue 3786]:
-Fix SQL statement syntax in schema migration.
-+
-An extra semicolon was preventing migration from 2.11.x to 2.12 when using
-an Oracle database.
-
-* Send email using email queue instead of the default queue.
-+
-Some emails sent asynchronously were already being sent using that queue
-but some were not. This was confusing for a gerrit administrator because
-if there is a build up of `send-email` tasks in the queue, he would
-think that increasing `sendemail.threadPoolSize` would help but it did not
-because some of the email were sent using the default queue which is
-configurable using `execution.defaultThreadPoolSize`.
-
-* Fix XSRF token cookie to honor `auth.cookieSecure` setting.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3767[Issue 3767]:
-Fix replication of first patch set for new changes.
-+
-When new changes were pushed from the command line, the first patch
-set did not get replicated to destinations.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3771[Issue 3771]:
-Remove `index.defaultMaxClauseCount` configuration option.
-+
-When `index.maxTerms` was either not set (thus no limit) or set to a value
-higher than `index.defaultMaxClauseCount` it was possible that viewing the
-related changes tab could cause a 'Too many clauses' error for changes that
-have a lot of related changes.
-+
-The `index.defaultMaxClauseCount` configuration option is removed, and the
-existing `index.maxTerms` is reused. The default value of `index.maxTerms`
-is reduced from 'no limit' to 1024.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3919[Issue 3919]:
-Explicitly set parent project to 'All-Projects' when a project is created
-without giving the parent.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3948[Issue 3948]:
-Fix submit of project parent updates on `refs/meta/config`.
-+
-When submitting a change on `refs/meta/config` to update a project's parent,
-the error 'The change must be submitted by a Gerrit administrator' was being
-displayed even when the submitter was an admin. The submit was successful
-when clicking 'Submit' a second time.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3811[Issue 3811]:
-Fix submittability of merge commits that resolve merge conflicts.
-+
-If a series of changes contained a change that conflicted with the destination
-branch, but the conflict was solved by a merge commit at the tip of the
-series, the series was not submittable.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3883[Issue 3883]:
-Respect the `core.commentchar` setting from `.gitconfig` in `commit-msg` hook.
-
-=== UI
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3894[Issue 3894]:
-Fix display of 'Related changes' after change is rebased in web UI:
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3071[Issue 3071]:
-Fix display of submodule differences in side-by-side view.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3718[Issue 3718]:
-Hide avatar images when no avatars are available.
-+
-The UI was showing a transparent empty image with a border.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3731[Issue 3731]:
-Fix syntax higlighting of tcl files.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3863[Issue 3863]:
-Fix display of active row marker in tag list.
-+
-Clicking on one of the rows would cause the tag name to disappear.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=1207[Issue 1207]:
-Fix keyboard shortcuts for non-US keyboards on side-by-side diff screen.
-+
-The forward/backward navigation keys `[` and `]` only worked on keyboards where
-these characters could be typed without using any modifier key (like CTRL, ALT,
-etc..).
-+
-Note that the problem still exists on the unified diff screen.
-
-* Improve tooltip on 'Submit' button when 'Submit whole topic' is enabled
-and the topic can't be submitted due to some changes not being ready.
-
-=== Plugins
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3821[Issue 3821]:
-Fix repeated reloading of plugins when running on OpenJDK 8.
-+
-OpenJDK 8 uses nanotime precision for file modification time on systems that
-are POSIX 2008 compatible. This leads to precision incompatibility when
-comparing the plugin's JAR file timestamp, resulting in the plugin being
-reloaded every minute.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3741[Issue 3741]:
-Fix handling of merge validation exceptions emitted by plugins.
-+
-If a plugin raised an exception, it was reported to the user as 'Change is
-new', rather than 'Missing dependency'.
-
-* Allow plugins to get the caller in merge validation requests.
-+
-Plugins that implement the `MergeValidationListener` interface now get the
-caller (the user who initiated the merge) in the `onPreMerge` method.
-+
-Existing plugins that implement this interface must be adapted to the new
-method signature.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3892[Issue 3892]:
-Allow plugins to suggest reviewers based on either change or project
-resources.
-
-=== Documentation
-
-* Update documentation of `commentlink` to reflect changed search URL.
-
-* Add missing documentation of valid `database.type` values.
-
-== Upgrades
-
-* Upgrade JGit to 4.1.2.201602141800-r.
+Release notes have been moved to the project homepage:
+link:https://www.gerritcodereview.com/releases/2.12.md#2.12.1[
+Release notes for Gerrit 2.12.1].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.2.txt b/ReleaseNotes/ReleaseNotes-2.12.2.txt
index 8292eb5..35682ed 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.2.txt
@@ -1,70 +1,5 @@
 = Release notes for Gerrit 2.12.2
 
-Gerrit 2.12.2 is now available:
-
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.12.2.war]
-
-== Schema Upgrade
-
-*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.1.html[
-2.12.1] but a manual schema upgrade is necessary when upgrading from 2.12.
-
-When upgrading a site that is already running version 2.12, the `patch_sets`
-table must be manually migrated using the `gerrit gsql` SSH command or the
-`gqsl` site program.
-
-For the default H2 database, execute the command:
-
-----
-  alter table patch_sets modify push_certficate clob;
-----
-
-For MySQL, execute the command:
-
-----
-  alter table patch_sets modify push_certficate text;
-----
-
-For PostgreSQL, execute the command:
-
-----
-  alter table patch_sets alter column push_certficate type text;
-----
-
-For other database types, execute the appropriate equivalent command.
-
-Note that the misspelled `push_certficate` is the actual name of the
-column.
-
-When upgrading from a version earlier than 2.12, or from 2.12.1 having already
-done the migration, this manual step is not necessary and should be omitted.
-
-
-== Bug Fixes
-
-* Upgrade Apache commons-collections to version 3.2.2.
-+
-Includes a fix for a link:https://issues.apache.org/jira/browse/COLLECTIONS-580[
-remote code execution exploit].
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3919[Issue 3919]:
-Explicitly set parent project to 'All-Projects' when a project is created
-without giving the parent.
-
-* Don't add message twice on abandon or restore via ssh review command.
-+
-When abandoning or reviewing a change via the ssh `review` command, and
-providing a message with the `--message` option, the message was added to
-the change twice.
-
-* Clear the input box after cancelling add reviewer action.
-+
-When the action was cancelled, the content of the input box was still
-there when opening it again.
-
-* Fix internal server error when aborting ssh command.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3969[Issue 3969]:
-Fix internal server error when submitting a change with 'Rebase If Necessary'
-strategy.
+Release notes have been moved to the project homepage:
+link:https://www.gerritcodereview.com/releases/2.12.md#2.12.2[
+Release notes for Gerrit 2.12.2].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.3.txt b/ReleaseNotes/ReleaseNotes-2.12.3.txt
index f51d739..06b18da 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.3.txt
@@ -1,113 +1,5 @@
 = Release notes for Gerrit 2.12.3
 
-Gerrit 2.12.3 is now available:
-
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.3.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.12.3.war]
-
-Gerrit 2.12.3 includes the bug fixes done with
-link:ReleaseNotes-2.11.8.html[Gerrit 2.11.8] and
-link:ReleaseNotes-2.11.9.html[Gerrit 2.11.9]. These bug fixes are *not*
-listed in these release notes.
-
-== Schema Upgrade
-
-*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.2.html[
-2.12.2] but a manual schema upgrade is necessary when upgrading from 2.12.
-
-When upgrading a site that is already running version 2.12, the `patch_sets`
-table must be manually migrated using the `gerrit gsql` SSH command or the
-`gqsl` site program.
-
-For the default H2 database, execute the command:
-
-----
-  alter table patch_sets modify push_certficate clob;
-----
-
-For MySQL, execute the command:
-
-----
-  alter table patch_sets modify push_certficate text;
-----
-
-For PostgreSQL, execute the command:
-
-----
-  alter table patch_sets alter column push_certficate type text;
-----
-
-For other database types, execute the appropriate equivalent command.
-
-Note that the misspelled `push_certficate` is the actual name of the
-column.
-
-When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2
-having already done the migration, this manual step is not necessary and
-should be omitted.
-
-
-== Bug Fixes
-
-* Fix SSL security issue in the SMTP email relay.
-+
-The hostname of the SSL socket was not verified. This made the read
-from the socket insecure since without verifying the hostname it may
-be link:https://www.cs.utexas.edu/~shmat/shmat_ccs12.pdf[vulnerable
-to a man-in-the-middle attack].
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3895[Issue 3895]:
-Fix failure to submit with 'Rebase if Necessary' after changes were reordered
-with interactive rebase.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4052[Issue 4052]:
-Fix failure to start server after upgrade from version 2.9.4.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3891[Issue 3891]:
-Fix query with `label:` operator and zero value.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4112[Issue 4112]:
-Fix failure to submit changes caused by empty user edit ref.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4087[Issue 4087]:
-Fix failure to submit change when a branch is created on the change ref.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4155[Issue 4155]:
-Fix tags REST API to correctly return all tags.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4154[Issue 4154]:
-Add support for `.team` and several more TLDs in email address validation.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4163[Issue 4163]:
-Prevent removal of non-voting reviewers on submit of change.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=2647[Issue 2647]:
-Fix usage of `CTRL-C` on change screen.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4236[Issue 4236]:
-Fix internal error when pushing an amended commit with the `%edit` option.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3426[Issue 3426]:
-Fix pushing changes with `%base` option or `newChangeForAllNotInTarget` option.
-
-* Show 'Submitted Together' tab for changes with same topic.
-
-* Improve submit button tooltip messages shown when change is not submittable.
-
-* Fix firing of the `topic-changed` hook.
-
-* Remove `--dry-run` option from the `Reindex` site program.
-+
-The implementation of the option was removed, but the option was mistakenly
-added back to the command and did not actually work.
-
-* Print proper task names in the output of the `show-queues` command.
-
-* Replication plugin: Double check if a ref is missing locally before deleting
-from remote.
-
-* Show an error message when trying to add a non-existent group to an ACL.
-
-== Updates
-
-* Update commons-validator to 1.5.1.
+Release notes have been moved to the project homepage:
+link:https://www.gerritcodereview.com/releases/2.12.md#2.12.3[
+Release notes for Gerrit 2.12.3].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.4.txt b/ReleaseNotes/ReleaseNotes-2.12.4.txt
index 64252c6..8321efa 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.4.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.4.txt
@@ -1,128 +1,5 @@
 = Release notes for Gerrit 2.12.4
 
-Gerrit 2.12.4 is now available:
-
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.4.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.12.4.war]
-
-== Schema Upgrade
-
-*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.3.html[
-2.12.3] but a manual schema upgrade is necessary when upgrading from 2.12.
-
-When upgrading a site that is already running version 2.12, the `patch_sets`
-table must be manually migrated using the `gerrit gsql` SSH command or the
-`gqsl` site program.
-
-For the default H2 database, execute the command:
-
-----
-  alter table patch_sets modify push_certficate clob;
-----
-
-For MySQL, execute the command:
-
-----
-  alter table patch_sets modify push_certficate text;
-----
-
-For PostgreSQL, execute the command:
-
-----
-  alter table patch_sets alter column push_certficate type text;
-----
-
-For other database types, execute the appropriate equivalent command.
-
-Note that the misspelled `push_certficate` is the actual name of the
-column.
-
-When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2
-having already done the migration, this manual step is not necessary and
-should be omitted.
-
-== Known Issues
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4323[Issue 4323]:
-'value too long for type character varying(255)' in patch_sets table when
-migrating to schema version 108.
-+
-This error may occur under some circumstances when running the schema
-migration from an earlier version of Gerrit.
-+
-On sites where this occurs, it can be fixed with a manual schema update
-according to the comments in the issue.
-
-== Bug Fixes
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4400[Issue 4400]:
-Fix `AlreadyClosedException` in Lucene index.
-+
-If a Lucene indexing thread was interrupted by an SSH connection being
-closed, this would also close file handles being used to read the index.
-+
-Lucene queries are now executed on background threads to isolate them
-from SSH threads.
-+
-This may also reduce latency for user dashboards on a multi-core system as
-each query for the different sections can now run on separate threads and
-return results when ready.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4249[Issue 4249]:
-Fix 'Duplicate stages not allowed' error during indexing.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4238[Issue 4238]:
-Fix 'not found' error when browsing tree in gitweb.
-+
-The `refs/heads/` prefix was incorrectly being added to `HEAD`, causing a
-'404 Not Found' error.
-
-* Allow to read repositories that do not end with `.git`.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4262[Issue 4262]:
-Fix GPG push certificate for first patch set of new changes.
-+
-The GPG certificate was not being set for the first patch set of new
-changes.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4296[Issue 4296]:
-Fix internal error when a query does not contain any token.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4241[Issue 4241]:
-Fix 'Cannot format velocity template' error when sending notification emails.
-
-* Fix `sshd.idleTimeout` setting being ignored.
-+
-The `sshd.idleTimeout` setting was not being correctly set on the SSHD
-backend, causing idle sessions to not time out.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4324[Issue 4324]:
-Set the correct uploader on new patch sets created via the inline editor.
-
-* Log a warning instead of failing when invalid commentlinks are configured.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4136[Issue 4136]:
-Fix support for `HEAD` requests in the REST API.
-+
-Sending a `HEAD` request failed with '404 Not Found'.
-
-* Return proper error response when trying to confirm an email that is already
-used by another user.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4318[Issue 4318]
-Fix 'Rebase if Necessary' merge strategy to prevent introducing a duplicate
-commit when submitting a merge commit.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4332[Issue 4332]:
-Allow `local` as a valid TLD for outgoing emails.
-
-* Bypass hostname verification when `sendemail.sslVerify` is disabled.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4398[Issue 4398]:
-Replication: Consider ref visibility when scheduling replication.
-+
-It was possible for refs to be replicated to remotes despite not being
-visible to groups mentioned in the `authGroup` setting.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4036[Issue 4036]:
-Fix hanging query when using `is:watched` without authentication.
+Release notes have been moved to the project homepage:
+link:https://www.gerritcodereview.com/releases/2.12.md#2.12.4[
+Release notes for Gerrit 2.12.4].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.5.txt b/ReleaseNotes/ReleaseNotes-2.12.5.txt
index 12d6870..4199fe0 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.5.txt
@@ -1,101 +1,5 @@
 = Release notes for Gerrit 2.12.5
 
-Gerrit 2.12.5 is now available:
-
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.12.5.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.12.5.war]
-
-== Schema Upgrade
-
-*WARNING:* There are no schema changes from link:ReleaseNotes-2.12.4.html[
-2.12.4] but a manual schema upgrade is necessary when upgrading from 2.12.
-
-When upgrading a site that is already running version 2.12, the `patch_sets`
-table must be manually migrated using the `gerrit gsql` SSH command or the
-`gqsl` site program.
-
-For the default H2 database, execute the command:
-
-----
-  alter table patch_sets modify push_certficate clob;
-----
-
-For MySQL, execute the command:
-
-----
-  alter table patch_sets modify push_certficate text;
-----
-
-For PostgreSQL, execute the command:
-
-----
-  alter table patch_sets alter column push_certficate type text;
-----
-
-For other database types, execute the appropriate equivalent command.
-
-Note that the misspelled `push_certficate` is the actual name of the
-column.
-
-When upgrading from a version earlier than 2.12, or from 2.12.1 or 2.12.2
-having already done the migration, this manual step is not necessary and
-should be omitted.
-
-== Known Issues
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4323[Issue 4323]:
-'value too long for type character varying(255)' in patch_sets table when
-migrating to schema version 108.
-+
-This error may occur under some circumstances when running the schema
-migration from an earlier version of Gerrit.
-+
-On sites where this occurs, it can be fixed with a manual schema update
-according to the comments in the issue.
-
-== New Features
-
-* New preference to enable line wrapping in diff screen and inline editor.
-
-== Bug Fixes
-
-* Fix the diff and edit preference dialogs for smaller screens.
-+
-On smaller screens the options at the bottom of the dialogs would
-get cut off, making it difficult to change them.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4521[Issue 4521]:
-Fix internal server error during validation of email addresses.
-+
-When creating a new account or adding a new email address to an existing
-account, the email validation crashed.
-
-* Lucene stability improvements.
-+
-Each Lucene index is now written using a dedicated background thread. Lucene
-threads may not be cancelled, to prevent interruptions while writing.
-
-* Don't try to change username that is already set.
-+
-Since Gerrit version 2.1.4 it is not allowed to change the username once
-it has been set, and attempting to do so results in an exception.
-+
-If `ldap.accountSshUserName` is set in the `gerrit.config` using
-`${userPrincipalName.localPart}` to initialize the username from the user's
-email address, and then the email address is changed, the username gets
-resolved to something different and the account manager tried to change it.
-As a result, an exception was raised and the user could no longer log in.
-+
-Instead of trying to change the username, a warning is logged.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4006[Issue 4006]:
-Prevent search limit parameter from exceeding maximum integer value.
-
-* Fix internal server error when generating task names.
-
-* Print proper names for query tasks in the output of the `show-queue` command.
-
-* Double-check change status when auto-abandoning changes.
-+
-It was possible that changes could be updated in the time between the query
-results being returned and the change being abandoned.
+Release notes have been moved to the project homepage:
+link:https://www.gerritcodereview.com/releases/2.12.md#2.12.5[
+Release notes for Gerrit 2.12.5].
diff --git a/ReleaseNotes/ReleaseNotes-2.12.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
index 84644e8..3eae5e4 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.txt
@@ -1,562 +1,5 @@
 = Release notes for Gerrit 2.12
 
-
-Gerrit 2.12 is now available:
-
-link:https://www.gerritcodereview.com/download/gerrit-2.12.war[
-https://www.gerritcodereview.com/download/gerrit-2.12.war]
-
-== Important Notes
-
-*WARNING:* This release contains schema changes.  To upgrade:
-----
-  java -jar gerrit.war init -d site_path
-----
-
-*WARNING:* To use online reindexing when upgrading to 2.12.x, the server must
-first be upgraded to 2.8 (or 2.9) and then through 2.10 and 2.11 to 2.12.x. If
-reindexing will be done offline, you may ignore this warning and upgrade directly
-to 2.12.x.
-
-*WARNING:* When upgrading from version 2.8.4 or older with a site that uses
-Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
-libraries should be manually removed from site's `lib` folder to prevent the
-startup failure described in
-link:https://code.google.com/p/gerrit/issues/detail?id=3084[issue 3084].
-
-*WARNING:* The Solr secondary index is no longer supported. With this release
-the only supported secondary index is Lucene.
-
-*WARNING:* The format of the `ref-updated` event has changed. Users of the
-link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[
-Jenkins Gerrit Trigger plugin] with jobs triggering on `ref-updated` should
-upgrade to at least
-link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger#GerritTrigger-Version2.15.1%28releasedSept142015%29[
-version 2.15.1]. If an upgrade of the plugin is not possible, a workaround is
-to change the branch configuration to type `Path` with a pattern like
-`refs/*/master` instead of `Plain` and `master`.
-
-
-== Release Highlights
-
-This release includes the following new features. See the sections below for
-further details.
-
-* New change submission workflows: 'Submit Whole Topic' and 'Submitted Together'.
-
-* Support for GPG Keys and signed pushes.
-
-
-== New Features
-
-=== New Change Submission Workflows
-
-* New 'Submit Whole Topic' setting.
-+
-When the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#change.submitWholeTopic[
-`change.submitWholeTopic`] setting is enabled, all changes belonging to the same
-topic will be submitted at the same time.
-+
-This setting should be considered experimental, and is disabled by default.
-
-* Submission of changes may include ancestors.
-+
-If a change is submitted that has submittable ancestor changes, those changes
-will also be submitted.
-
-* The merge queue is removed.
-+
-Changes that cannot be submitted due to missing dependencies will no longer
-enter the 'Submitted, Merge Pending' state.
-
-
-=== GPG Keys and Signed Pushes
-
-* Signed push can be enabled by setting
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.enableSignedPush[
-`receive.enableSignedPush`] to true.
-+
-When a client pushes with `git push --signed`, Gerrit ensures that the push
-certificate is valid and signed with a valid public key stored in the
-`refs/meta/gpg-keys` branch of the `All-Users` repository.
-
-* When signed push is enabled, and
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#gerrit.editGpgKeys[
-`gerrit.editGpgKeys`] is set to true, users may upload their public GPG
-key via the REST API or UI.
-+
-If this setting is not enabled, GPG keys may only be added by administrators
-with direct access to the `All-Users` repository.
-
-* Administrators may also configure
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.certNonceSeed[
-`receive.certNonceSeed`] and
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.certNonceSlop[
-`receive.certNonceSlop`].
-
-
-=== Secondary Index
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3333[Issue 3333]:
-Support searching for changes by author and committer.
-+
-Changes are indexed by the git author and committer of the latest patch set,
-and can be searched with the `author:` and `committer:` operators.
-+
-Changes are matched on either the exact whole email address, or on parts of the
-name or email address.
-
-* Add `from:` search operator to match by owner of change or author of comments.
-
-* Add `commentby:` search operator to search by author of comments.
-
-* Change the `topic:` search operator to search by the exact topic name.
-
-* Add `intopic:` search operator to search by topics containing the search term.
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3291[Issue 3291]:
-Add `has:edit` search operator to match changes that have edit revisions on them.
-
-* Allow configuration of maximum query size.
-+
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#index.maxTerms[
-`index.maxTerms`] can be set to limit the number of leaf index terms.
-
-* Expose Lucene index writers for plugins.
-+
-Plugins can now reconfigure various Lucene performance related parameters
-at runtime.
-
-* Make Lucene index writers auto-commit writers.
-+
-Plugins can now temporarily turn on auto-committing in situations where it makes
-sense to enforce all changes to be written to disk ASAP.
-
-
-=== UI
-
-==== General
-
-* Edit and diff preferences can be modified from the user preferences screen.
-+
-Previously it was only possible to edit these preferences from the actual
-diff and edit screens.
-
-* Add 'Edits' to the 'My' dashboard menu to list changes on which the user
-has an unpublished edit revision.
-
-* Support for URL aliases.
-+
-Administrators may define
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#urlAlias[
-URL aliases] to map plugin screens into the Gerrit URL namespace.
-+
-Plugins may use user-specific URL aliases to replace certain screens for certain
-users.
-
-
-==== Project Screen
-
-* New tab to list the project's tags, similar to the branch list.
-
-
-==== Inline Editor
-
-* Store and load edit preferences in git.
-+
-Edit preferences are stored and loaded to/from the `All-Users` repository.
-
-* Add 'auto close brackets' feature.
-
-* Add 'match brackets' feature.
-
-* Make the cursor blink rate customizable.
-
-* Add support for Emacs and Vim key maps.
-
-
-==== Change Screen
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3318[Issue 3318]:
-Highlight 'Reply' button if there are draft comments on any patch set.
-+
-If any patch set of the change has a draft comment by the current user,
-the 'Reply' button is highlighted.
-+
-The icons depicting draft comments are removed from the revisions drop-down
-list.
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=1100[Issue 1100]:
-Publish all draft comments when replying to a change.
-+
-All draft comments, including those on older patch sets, are published when
-replying to a change.
-
-* Show file size increase/decrease for binary files.
-
-* Show uploader if different from change owner.
-
-* Show push certificate status.
-
-* Show change subject as tooltip on related changes list.
-+
-This helps to identify changes when the subject is truncated in the list.
-
-
-==== Side-By-Side Diff
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3293[Issue 3293]:
-Add syntax highlighting for Puppet.
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3447[Issue 3447]:
-Add syntax highlighting for VHDL.
-
-
-==== Group Screen
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=1479[Issue 1479]:
-The group screen now includes an 'Audit Log' panel showing member additions,
-removals, and the user who made the change.
-
-
-=== API
-
-Several new APIs are added.
-
-==== Accounts
-
-* Suggest accounts.
-
-==== Tags
-
-* List tags.
-
-* Get tag.
-
-
-=== REST API
-
-New REST API endpoints and new options on existing endpoints.
-
-
-==== Accounts
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#set-username[
-Set Username]: Set the username of an account.
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#get-detail[
-Get Account Details]: Get the details of an account.
-+
-In addition to the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#account-info[
-AccountInfo] fields returned by the existing
- link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-accounts.html#get-account[
-Get Account] endpoint, the new REST endpoint returns the registration date of
-the account and the timestamp of when contact information was filed for this
-account.
-
-
-==== Changes
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[
-Set Review]: Add an option to omit duplicate comments.
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#get-safe-content[
-Download Content]: Download the content of a file from a certain revision, in a
-safe format that poses no risk for inadvertent execution of untrusted code.
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#submitted-together[
-Get Submitted Together]: Get the list of all changes that will be submitted at
-the same time as the change.
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=1100[Issue 1100]:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#set-review[
-Set Review]: Add an option to publish draft comments on all revisions.
-
-==== Config
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#get-info[
-Get Server Info]: Return information about the Gerrit server configuration.
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#confirm-email[
-Confirm Email]: Confirm that the user owns an email address.
-
-
-==== Groups
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-groups.html#list-group[
-List Groups]: Add option to suggest groups.
-+
-This allows group auto-completion to be used in a plugin's UI.
-
-*  link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-groups.html#get-audit-log[
-Get Audit Log]: Get the audit log of a Gerrit internal group, showing member
-additions, removals, and the user who made the change.
-
-
-==== Projects
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-projects.html#run-gc[
-Run GC]: Add `aggressive` option to specify whether or not to run an aggressive
-garbage collection.
-
-* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-projects.html#list-tags[
-List Tags]: Support filtering by substring and regex, and pagination with
-`--start` and `--end`.
-
-
-=== SSH
-
-* Add support for ZLib Compression.
-+
-To enable compression use the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#sshd.enableCompression[
-`sshd.enableCompression` setting].
-
-* Add support for hmac-sha2-256 and hmac-sha2-512 as MACs.
-
-=== Plugins
-
-==== General
-
-* Gerrit client can now pass JavaScriptObjects to extension panels.
-
-* New UI extension point for header bar in change screen.
-
-* New UI extension point to password screen.
-
-* New UI extension points to project info screen.
-
-* New UI extension point for pop down buttons on change screen.
-
-* New UI extension point for buttons in header bar on change screen.
-
-* New UI extension point at bottom of the user preferences screen.
-
-* New UI extension point for the 'Included In' drop-down panel.
-+
-By implementing the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/dev-plugins.html#included-in[
-Included In interface], plugins may add entries to the 'Included In' dropdown
-menu on the change screen.
-
-* Plugins can extend Gerrit screens with GWT controls.
-
-* Plugins can add custom settings screens.
-
-* Referencing groups in `project.config`.
-+
-Plugins can refer to groups so that when they are renamed, the project
-config will also be updated in this section.
-
-* API
-
-** Allow to use `CurrentSchemaVersion`.
-
-** Allow to use `InternalChangeQuery.query()`.
-
-** Allow to use `JdbcUtil.port()`.
-
-** Allow to use GWTORM `Key` classes.
-
-
-=== Other
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3401[Issue 3401]:
-Add option to
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#sendemail.allowRegisterNewEmail[
-disable registration of new email addresses].
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=2061[Issue 2061]
-Add Support for `git-upload-archive`.
-+
-This allows use the standard `git archive` command to create an archive
-of the content of a repository.
-
-* Add a background job to automatically abandon inactive changes.
-+
-The
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#changeCleanup[
-changeCleanup] configuration can be set to periodically check for inactive
-changes and automatically abandon them.
-
-* Add support for the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/database-setup.html#createdb_db2[
-DB2 database].
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3441[Issue 3441]:
-Add support for the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/database-setup.html#createdb_derby[
-Apache Derby database].
-
-* Download commands plugin: Use commit IDs for download commands when change refs are hidden.
-+
-Git has a configuration option to hide refs from the initial advertisement
-(`uploadpack.hideRefs`). This option can be used to hide the change refs from
-the client. As consequence this prevented fetching changes by change ref from
-working.
-+
-Setting `download.checkForHiddenChangeRefs` in the `gerrit.config` to true
-allows the download commands plugin to check for hidden change refs.
-
-* Add a new 'Maintain Server' global capability.
-+
-Members of a group with the 'Maintain Server' capability may view caches, tasks,
-and queues, and invoke the index REST API on changes.
-
-
-== Bug Fixes
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3499[Issue 3499]:
-Fix syntax highlighting of raw string literals in go.
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3643[Issue 3643]:
-Fix syntax highlighting of ES6 string templating using backticks.
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3653[Issue 3653]:
-Correct timezone in sshd log after DST change.
-+
-When encountering a DST switch, the timezone wasn't updated until
-the server was reloaded.
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3306[Issue 3306]:
-Allow admins to read, push and create on `refs/users/default`.
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3212[Issue 3212]:
-Fix failure to run `init` when `--site-path` option is not explicitly given.
-
-* Make email validation case insensitive.
-+
-While link:https://tools.ietf.org/html/rfc5321#section-2.3.11[
-RFC 5321 section 2.3.11] allows for the local-part (the part left of
-the '@') of an email address to be case sensitive, the domain portion is
-case insensitive according to
-link:https://tools.ietf.org/html/rfc1035#section-3.1[RFC 1035 section 3.1].
-And in practice, even the local-part is typically case insensitive also.
-
-* `commit-msg` hook: Don't add `Change-Id` line on temporary commits.
-+
-Commits created with `git commit --fixup` or `git commit --squash` are not
-intended to be pushed to Gerrit, and don't need a `Change-Id` line.
-+
-This also prevents changes from being accidentally uploaded, at least for
-projects that have the 'Require Change-Id' configuration enabled.
-
-* link:http://code.google.com/p/gerrit/issues/detail?id=3444[Issue 3444]:
-download-commands plugin: Fix clone with commit-msg hook when project name
-contains '/'.
-
-* Use full ref name in `refName` attribute of `ref-updated` events.
-+
-The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/json.html#refUpdate[
-refUpdate attribute] in `ref-updated` events did not include the full name
-of the ref in the `refName` attribute, i.e. `master` was used instead of
-`refs/heads/master`.
-+
-Support for the new format is added in
-link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger#GerritTrigger-Version2.15.1%28releasedSept142015%29[
-version 2.15.1 of the Jenkins Gerrit Trigger plugin].
-+
-Users who are unable to upgrade the plugin may instead change the
-trigger's branch configuration to type `Path` with a pattern like
-`refs/*/master` instead of `Plain` and `master`.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3714[Issue 3714]:
-Improve visibility of comments on dark themes.
-
-* Fix highlighting of search results and trailing whitespaces in intraline
-diff chunks.
-
-* Fix server error when listing annotated/signed tag that has no tagger info.
-
-* Don't create new account when claimed OAuth identity is unknown.
-+
-The Claimed Identity feature was enabled to support old Google OpenID accounts,
-that cannot be activated anymore. In some corner cases, when for example the URL
-is not from the production Gerrit site, for example on a staging instance, the
-OpenID identity may deviate from the original one. In case of mismatch, the lookup
-of the user for the claimed identity would fail, causing a new account to be
-created.
-
-* Suggest to upgrade installed plugins per default during site initialization
-to new Gerrit version.
-+
-The default was 'No' which resulted in some sites not upgrading core
-plugins and running the wrong versions.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3698[Issue 3698]:
-Fix creation of the administrator user on databases with pre-allocated
-auto-increment column values.
-+
-When using a database configuration where auto-increment column values are
-pre-allocated, it was possible that the 'Administrators' group was created
-with an ID other than `1`. In this case, the created admin user was not added
-to the correct group, and did not have the correct admin permissions.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=3018[Issue 3018]:
-Fix query for changes using a label with a group operator.
-+
-The `group` operator was being ignored when searching for changes with labels
-because the search index does not contain group information.
-
-* Fix online reindexing of changes that don't already exist in the index.
-+
-Changes are now always reloaded from the database during online reindex.
-
-* Fix reading of plugin documentation.
-+
-Under some circumstances it was possible to fail with an IO error.
-
-== Documentation Updates
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=412[Issue 412]:
-Update documentation of `commentlink.match` regular expression to clarify
-that the expression is applied to the rendered HTML.
-
-* Remove warning about unstable change edit REST API endpoints.
-+
-These endpoints should be considered stable since version 2.11.
-
-* Document that `ldap.groupBase` and `ldap.accountBase` are repeatable.
-
-== Upgrades
-
-* Upgrade Asciidoctor to 1.5.2
-
-* Upgrade AutoValue to 1.1
-
-* Upgrade Bouncy Castle to 1.52
-
-* Upgrade CodeMirror to 5.7
-
-* Upgrade gson to 2.3.1
-
-* Upgrade guava to 19.0-RC2
-
-* Upgrade gwtorm to 1.14-20-gec13fdc
-
-* Upgrade H2 to 1.3.176
-
-* Upgrade httpcomponents to 4.4.1
-
-* Upgrade Jetty to 9.2.13.v20150730
-
-* Upgrade JGit to 4.1.1.201511131810-r
-
-* Upgrade joda-time to 2.8
-
-* Upgrade JRuby to 1.7.18
-
-* Upgrade jsch to 0.1.53
-
-* Upgrade JUnit to 4.11
-
-* Upgrade Lucene to 5.3.0
-
-* Upgrade Prolog Cafe 1.4.1
-
-* Upgrade servlet API to 8.0.24
-
-* Upgrade Truth to version 0.27
-
+Release notes have been moved to the project homepage:
+link:https://www.gerritcodereview.com/releases/2.12.md[
+Release notes for Gerrit 2.12].
diff --git a/ReleaseNotes/ReleaseNotes-2.13.1.txt b/ReleaseNotes/ReleaseNotes-2.13.1.txt
index 958e726..7b27ad3 100644
--- a/ReleaseNotes/ReleaseNotes-2.13.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.13.1.txt
@@ -1,21 +1,5 @@
 = Release notes for Gerrit 2.13.1
 
-Gerrit 2.13.1 is now available:
-
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.13.1.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.13.1.war]
-
-== Schema Upgrade
-
-There are no schema changes from link:ReleaseNotes-2.13.html[2.13].
-
-== Bug Fixes
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4618[Issue 4618]:
-Fix internal server error after online reindexing completed.
-
-* Fix internal server error when cloning from slaves and not all refs are
-visible.
-
-* Fix JSON deserialization error causing stream event client to no longer receive
-events.
+Release notes have been moved to the project homepage:
+link:https://www.gerritcodereview.com/releases/2.13.md#2.13.1[
+Release notes for Gerrit 2.13.1].
diff --git a/ReleaseNotes/ReleaseNotes-2.13.2.txt b/ReleaseNotes/ReleaseNotes-2.13.2.txt
index c7be976..72bd218 100644
--- a/ReleaseNotes/ReleaseNotes-2.13.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.13.2.txt
@@ -1,46 +1,5 @@
 = Release notes for Gerrit 2.13.2
 
-Gerrit 2.13.2 is now available:
-
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.13.2.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.13.2.war]
-
-== Schema Upgrade
-
-There are no schema changes from link:ReleaseNotes-2.13.1.html[2.13.1].
-
-== Bug Fixes
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4630[Issue 4630]:
-Fix server error when navigating up to change while 'Working' is displayed.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4631[Issue 4631]:
-Read project watches from database.
-+
-Project watches were being read from the git backend by default, but the
-migration to git is not yet completed.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4632[Issue 4632]:
-Fix server error when deleting multiple SSH keys from the Web UI.
-+
-Attempting to delete multiple keys in parallel resulted in a lock failure
-when removing the keys from the git backend.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4645[Issue 4645]:
-Fix malformed account suggestions.
-+
-If the query contained several query terms and one of the query terms was
-a substring of 'strong', the suggestion was malformed.
-
-* Hooks plugin: Fix incorrect value passed to `--change-url` parameter.
-+
-The URL was being generated using the change's Change-Id rather than the
-change number.
-
-* Check for CLA when creating project config changes from the web UI.
-+
-If contributor agreements were enabled and required for a project, and
-the user had not signed a CLA, it was still possible to upload changes
-for review on `refs/meta/config` by making changes in the project access
-editor and pressing 'Save for Review'.
-
+Release notes have been moved to the project homepage:
+link:https://www.gerritcodereview.com/releases/2.13.md#2.13.2[
+Release notes for Gerrit 2.13.2].
diff --git a/ReleaseNotes/ReleaseNotes-2.13.txt b/ReleaseNotes/ReleaseNotes-2.13.txt
index 0afca1a..b3e125d 100644
--- a/ReleaseNotes/ReleaseNotes-2.13.txt
+++ b/ReleaseNotes/ReleaseNotes-2.13.txt
@@ -1,471 +1,5 @@
 = Release notes for Gerrit 2.13
 
-
-Gerrit 2.13 is now available:
-
-link:https://www.gerritcodereview.com/download/gerrit-2.13.war[
-https://www.gerritcodereview.com/download/gerrit-2.13.war]
-
-
-== Important Notes
-
-*WARNING:* This release contains schema changes.  To upgrade:
-----
-  java -jar gerrit.war init -d site_path
-----
-
-*WARNING:* To use online reindexing for `changes` secondary index when upgrading
-to 2.13.x, the server must first be upgraded to 2.8 (or 2.9) and then through
-2.10, 2.11 and 2.12. Skipping a version will prevent the online reindexer from
-working.
-
-Gerrit 2.13 introduces a new secondary index for accounts, and this must be
-indexed offline before starting Gerrit:
-----
-  java -jar gerrit.war reindex --index accounts -d site_path
-----
-
-If reindexing will be done offline, you may ignore these warnings and upgrade
-directly to 2.13.x using the following command that will reindex both `changes`
-and `accounts` secondary indexes:
-----
-  java -jar gerrit.war reindex -d site_path
-----
-
-*WARNING:* The server side hooks functionality is moved to a core plugin. Sites
-that make use of server side hooks must install this plugin during site init.
-
-
-== Release Highlights
-
-* Support for Large File Storage (LFS).
-
-* Metrics interface.
-
-* Hooks plugin.
-
-* Secondary index for accounts.
-
-* File annotations (blame) in side-by-side diff.
-
-== New Features
-
-=== Large File Storage (LFS)
-
-Gerrit provides an
-link:https://gerrit-review.googlesource.com/Documentation/2.13/dev-plugins.html#lfs-extension[
-extension point] that enables development of plugins implementing the
-link:https://github.com/github/git-lfs/blob/master/docs/api/v1/http-v1-batch.md[
-LFS protocol].
-
-By setting
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#lfs.plugin[
-`lfs.plugin`] the administrator can configure the name of the plugin
-which handles LFS requests.
-
-=== Access control for git submodule subscriptions
-
-To prevent potential security breaches as described in
-link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3311[issue 3311],
-it is now only possible for a project to subscribe to a submodule if the
-submodule explicitly allows itself to be subscribed.
-
-Please see the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/user-submodules.html[
-submodules user guide] for details.
-
-Note that when upgrading from an earlier version of Gerrit, permissions for
-any existing subscriptions will be automatically added during the database
-schema migration.
-
-=== Metrics
-
-Metrics about Gerrit's internal state can be sent to external
-monitoring systems.
-
-Plugins can provide implementations of the metrics interface to
-report metrics to different monitoring systems. The following
-plugins are available:
-
-* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-jmx[
-JMX]
-
-* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-graphite[
-Graphite]
-
-* link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/metrics-reporter-elasticsearch[
-Elasticsearch]
-
-Plugins can also provide their own metrics.
-
-See the link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/metrics.html[
-metrics documentation] for further details.
-
-=== Hooks
-
-Server side hooks are moved to the
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/hooks[
-hooks plugin]. Sites that make use of server side hooks should install this
-plugin. After installing the plugin, no additional configuration is needed.
-The plugin uses the same configuration settings in `gerrit.config`.
-
-=== Secondary Index
-
-* The secondary index now supports indexing of accounts.
-+
-The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/pgm-reindex.html[
-reindex program] by default reindexes all changes and accounts. A new
-option allows to explicitly specify whether to reindex changes or accounts.
-+
-The `suggest.fullTextSearch`, `suggest.fullTextSearchMaxMatches` and
-`suggest.fullTextSearchRefresh` configuration options are removed. Full text
-search is supported by default with the account secondary index.
-
-* New ssh command to
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/cmd-index-changes.html[
-reindex changes].
-
-
-=== UI
-
-* The UI can now be loaded in an iFrame by enabling
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#gerrit.canLoadInIFrame[
-gerrit.canLoadInIFrame] in the site configuration.
-
-==== Change Screen
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=106[Issue 106]:
-Allow to select merge commit's parent for diff base in change screen.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3035[Issue 3035]:
-Allow to remove specific votes from a change, while leaving the reviewer on the
-change.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3487[Issue 3487]:
-Use 'Ctrl-Alt-e' instead of 'e' to open edit mode.
-
-==== Diff Screens
-
-* Add all syntax highlighting available in CodeMirror.
-
-* Improve search experience in diff screen
-+
-Ctrl-F, Ctrl-G and Shift-Ctrl-G now bind to the search dialog box provided by
-CodeMirror's search add-on. Enter and Shift-Enter navigate among the search
-results from the CodeMirror search, just like they do in a normal browser
-search. Esc now clears the search result.
-+
-If the user sets `Render` to `Slow` in the diff preferences and the file is less
-than 4000 lines (huge), then Ctrl-F, Ctrl-G and Shift-Ctrl-G fall back to the
-browser search.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=2968[Issue 2968]:
-Allow to go back to change list by keyboard shortcut from diff screens.
-
-==== Side-By-Side Diff Screen
-
-* Blame annotations
-+
-By enabling
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#change.allowBlame[
-`change.allowBlame`], blame annotations can be shown in the side-by-side diff
-screen gutter. Clicking the annotation opens the relevant change.
-
-==== User Preferences
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=989[Issue 989]:
-New option to control email notifications.
-+
-Users can now choose between 'Enabled', 'Disabled' and 'CC Me on Comments I Write'.
-
-* New option to control adding 'Signed-off-by' footer in commit message of new changes
-created online.
-
-* New option to control auto-indent width in inline editor.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=890[Issue 890]:
-New diff option to control whether to skip unchanged files when navigating to
-the previous or the next file.
-
-=== Changes
-
-In order to avoid potentially confusing behavior, when submitting changes in a
-batch, submit type rules may not be used to mix submit types on a single branch,
-and trying to submit such a batch will fail.
-
-=== REST API
-
-==== Accounts
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3766[Issue 3766]:
-Allow users with the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/access-control.html#capability_modifyAccount[
-'ModifyAccount' capability] to get the preferences for other users via the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-user-preferences[
-Get User Preferences] endpoint.
-
-* Rename 'Suggest Account' to
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#query-account[
-'Query Account'] and add support for arbitrary account queries.
-+
-The `_more_accounts` flag is set on the last result when there are more results
-than the limit. The `DETAILS` and `ALL_EMAILS` options may be set to control
-whether the results should include details (full name, email, username, avatars)
-and all emails, respectively.
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-watched-projects[
-Get Watched Projects].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#set-watched-projects[
-Set Watched Projects].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#delete-watched-projects[
-Delete Watched Projects].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-stars[
-Get Star Labels from Change].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#set-stars[
-Update Star Labels on Change].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#get-oauth-token[
-Get OAuth Access Token].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#list-contributor-agreements[
-List Contributor Agreements].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-accounts.html#sign-contributor-agreement[
-Sign Contributor Agreement].
-
-==== Changes
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3579[Issue 3579]:
-Append submitted info to ChangeInfo.
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-changes.html#move-change[
-Move Change].
-
-==== Groups
-
-* Add `-s` as an alias for `--suggest` on the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-groups.html#suggest-group[
-Suggest Group] endpoint.
-
-==== Projects
-
-* Add `async` option to the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#run-gc[
-Run GC] endpoint to allow garbage collection to run asynchronously.
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#get-access[
-List Access Rights].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#set-access[
-Add, Update and Delete Access Rights].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#create-tag[
-Create Tag].
-
-* New endpoint:
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/rest-api-projects.html#get-mergeable-info[
-Get Mergeable Information].
-
-=== Plugins
-
-* Secure settings
-+
-Plugins may now store secure settings in `etc/$PLUGIN.secure.config` where they
-will be decoded by the Secure Store implementation.
-
-* Exported dependencies
-+
-Gson is now an exported dependency. Plugins no longer need to explicitly add
-a dependency on it.
-
-=== Misc
-
-* New project option to reject implicit merge commits.
-+
-The 'Reject Implicit Merges' option can be enabled to prevent non-merge commits
-from implicitly bringing unwanted changes into a branch. This can happen for
-example when a commit is made based on one branch but is mistakenly pushed to
-another, for example based on `refs/heads/master` but pushed to `refs/for/stable`.
-
-* New link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/access-control.html#category_add_patch_set[
-Add Patch Set capability] to control who is allowed to upload a new patch
-set to an existing change.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4015[Issue 4015]:
-Allow setting a
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/user-upload.html#message[
-comment message] when uploading a change.
-
-* Allow to specify
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/user-upload.html#notify[
-who should be notified by email] when uploading a change.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3220[Issue 3220]:
-Append approval info to every comment-added stream event and hook.
-
-* The `administrateServer` capability can be assigned to groups by setting
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#capability.administrateServer[
-capability.administrateServer] in the site configuration.
-+
-Configuring this option can be a useful fail-safe to recover a server in the
-event an administrator removed all groups from the `administrateServer`
-capability, or to ensure that specific groups always have administration
-capabilities.
-
-* New configuration options to configure JGit repository cache parameters.
-+
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#core.repositoryCacheCleanupDelay[
-core.repositoryCacheCleanupDelay] and
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/config-gerrit.html#core.repositoryCacheExpireAfter[
-core.repositoryCacheExpireAfter] can be configured.
-
-* Accept `-b` as an alias of `--batch` in the
-link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.13/pgm-init.html[
-init program].
-
-
-== Bug Fixes
-
-* Don't add the same SSH key multiple times.
-+
-If an already existing SSH key was added, a duplicate entry was added to the
-list of user's SSH keys.
-
-* Respect the 'Require a valid contributor agreement to upload' setting
-when creating changes via the UI.
-+
-If a user had not signed a CLA, it was still possible for them to create a new
-change with the 'Revert' or 'Cherry Pick' button.
-
-* Make Lucene index more stable when being interrupted.
-
-* Don't show the `start` and `idle` columns in the `show-connections`
-output when the ssh backend is NIO2.
-+
-The NIO2 backend doesn't provide the start and idle times, and the
-values being displayed were just dummy values. Now these values are
-only displayed for the MINA backend.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4150[Issue 4150]:
-Deleting a draft inline comment no longer causes the change's `Updated` field to
-be bumped.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4099[Issue 4099]:
-Fix SubmitWholeTopic does not update subscriptions.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3603[Issue 3603]:
-Fix editing a submodule via inline edit.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4069[Issue 4069]:
-Fix highlights in scrollbar overview ruler not moved when extending the
-displayed area.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3446[Issue 3446]:
-Respect the `Skip Deleted` diff preference.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3445[Issue 3445]:
-Respect the `Skip Uncommented` diff preference.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4051[Issue 4051]:
-Fix empty `From` email header.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3423[Issue 3423]:
-Fix intraline diff for added spaces.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=1867[Issue 1867]:
-Remove `no changes made` error case when the only difference between a new
-commit and the previous patch set of the change is the committer.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3831[Issue 3831]:
-Prevent creating groups with the same name as a system group.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=3754[Issue 3754]:
-Fix `View All Accounts` permission to allow accounts REST endpoint to access
-email info.
-
-* Make `gitweb.type` default to `disabled` when not explicitly set.
-+
-Previously the behavior was not documented and it would default to type
-`gitweb`. In cases where there was no gitweb config at all, this would
-result in broken links due to `null` being used as the URL.
-
-* link:https://bugs.chromium.org/p/gerrit/issues/detail?id=4488[Issue 4488]:
-Improve error message when `Change-Id` line is missing in commit message.
-+
-The error message now includes the sha1 of the commit, so that it is
-easier to track down which commit failed validation when multiple commits
-are pushed at the same time.
-
-* Don't check mergeability of draft changes.
-+
-Draft changes can be deleted but not abandoned so there is no way for
-an administrator to get rid of the them on behalf of the users. This can
-become a problem when there many draft changes because the mergeability
-check can be costly.
-+
-The mergeability check is no longer done for draft changes, but will be
-done when the draft change is published.
-
-* Fix internal server error when plugin-provided file history weblink
-is null.
-+
-It is valid for a plugin to provide a null weblink, but doing so resulted
-in an internal server error.
-
-== Dependency updates
-
-* Add dependency on blame-cache 0.1-9
-
-* Add dependency on guava-retrying 2.0.0
-
-* Add dependency on jsr305 3.0.1
-
-* Add dependency on metrics-core 3.1.2
-
-* Upgrade auto-value to 1.3-rc1
-
-* Upgrade commons-net to 3.5
-
-* Upgrade CodeMirror to 5.17.0
-
-* Upgrade Guava to 19.0
-
-* Upgrade Gson to 2.7
-
-* Upgrade Guice to 4.1.0
-
-* Upgrade gwtjsonrpc to 1.9
-
-* Upgrade gwtorm to 1.15
-
-* Upgrade javassist to 3.20.0-GA
-
-* Upgrade Jetty to 9.2.14.v20151106
-
-* Upgrade JGit to 4.5.0.201609210915-r
-
-* Upgrade joda-convert to 1.8.1
-
-* Upgrade joda-time to 2.9.4
-
-* Upgrade Lucene to 5.5.0
-
-* Upgrade mina to 2.0.10
-
-* Upgrade sshd-core to 1.2.0
+Release notes have been moved to the project homepage:
+link:https://www.gerritcodereview.com/releases/2.13.md[
+Release notes for Gerrit 2.13].
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 945f09f..9c28697 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -2,18 +2,18 @@
 
 [[s2_13]]
 == Version 2.13.x
-* link:ReleaseNotes-2.13.2.html[2.13.2]
-* link:ReleaseNotes-2.13.1.html[2.13.1]
-* link:ReleaseNotes-2.13.html[2.13]
+* link:https://www.gerritcodereview.com/releases/2.13.md#2.13.2[2.13.2]
+* link:https://www.gerritcodereview.com/releases/2.13.md#2.13.1[2.13.1]
+* link:https://www.gerritcodereview.com/releases/2.13.md[2.13]
 
 [[s2_12]]
 == Version 2.12.x
-* link:ReleaseNotes-2.12.5.html[2.12.5]
-* link:ReleaseNotes-2.12.4.html[2.12.4]
-* link:ReleaseNotes-2.12.3.html[2.12.3]
-* link:ReleaseNotes-2.12.2.html[2.12.2]
-* link:ReleaseNotes-2.12.1.html[2.12.1]
-* link:ReleaseNotes-2.12.html[2.12]
+* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.5[2.12.5]
+* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.4[2.12.4]
+* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.3[2.12.3]
+* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.2[2.12.2]
+* link:https://www.gerritcodereview.com/releases/2.12.md#2.12.1[2.12.1]
+* link:https://www.gerritcodereview.com/releases/2.12.md[2.12]
 
 [[s2_11]]
 == Version 2.11.x
diff --git a/WORKSPACE b/WORKSPACE
index 4487821..2ef3235 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -309,8 +309,8 @@
 
 maven_jar(
   name = 'commons_compress',
-  artifact = 'org.apache.commons:commons-compress:1.7',
-  sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d',
+  artifact = 'org.apache.commons:commons-compress:1.12',
+  sha1 = '84caa68576e345eb5e7ae61a0e5a9229eb100d7b',
 )
 
 maven_jar(
@@ -964,10 +964,12 @@
 
 npm_binary(
   name = "vulcanize",
+  repository = "GERRIT",
 )
 
 npm_binary(
   name = "crisper",
+  repository = "GERRIT",
 )
 
 # bower_archive() seed components.
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 7c6ce0e..d995be6 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
@@ -61,11 +61,11 @@
 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.MergePatchSetInput;
 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.MergeInput;
+import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
index 9dccf0c..3dc41fe 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -16,14 +16,27 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 
 /**
  * Describes the state required to submit a change.
  */
 public class SubmitRecord {
+  public static Optional<SubmitRecord> findOkRecord(
+      Collection<SubmitRecord> in) {
+    if (in == null) {
+      return Optional.empty();
+    }
+    return in.stream().filter(r -> r.status == Status.OK).findFirst();
+  }
+
   public enum Status {
+    // NOTE: These values are persisted in the index, so deleting or changing
+    // the name of any values requires a schema upgrade.
+
     /** The change is ready for submission. */
     OK,
 
@@ -50,6 +63,9 @@
 
   public static class Label {
     public enum Status {
+      // NOTE: These values are persisted in the index, so deleting or changing
+      // the name of any values requires a schema upgrade.
+
       /**
        * This label provides what is necessary for submission.
        * <p>
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 723b5e3..dd272fa 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -17,6 +17,8 @@
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
 import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
 import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.codec.binary.Base64.decodeBase64;
 
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
@@ -44,6 +46,7 @@
 import com.google.gerrit.server.index.change.ChangeField.PatchSetProtoField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -148,7 +151,7 @@
   private static <T> List<T> decodeProtos(JsonObject doc, String fieldName,
       ProtobufCodec<T> codec) {
     return FluentIterable.from(doc.getAsJsonArray(fieldName))
-        .transform(i -> codec.decode(Base64.decodeBase64(i.toString())))
+        .transform(i -> codec.decode(decodeBase64(i.toString())))
         .toList();
   }
 
@@ -383,7 +386,27 @@
         cd.setReviewers(ReviewerSet.empty());
       }
 
+      decodeSubmitRecords(source,
+          ChangeField.STORED_SUBMIT_RECORD_STRICT.getName(),
+          ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd);
+      decodeSubmitRecords(source,
+          ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(),
+          ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
+
       return cd;
     }
+
+    private void decodeSubmitRecords(JsonObject doc, String fieldName,
+        SubmitRuleOptions opts, ChangeData out) {
+      JsonArray records = doc.getAsJsonArray(fieldName);
+      if (records == null) {
+        return;
+      }
+      ChangeField.parseSubmitRecords(
+          FluentIterable.from(records)
+              .transform(i -> new String(decodeBase64(i.toString()), UTF_8))
+              .toList(),
+          opts, out);
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 8e36a77..bd5dd69 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java
index d5a9c1f..6e79c3a 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java
@@ -22,4 +22,12 @@
   public BadRequestException(String msg) {
     super(msg);
   }
+
+  /**
+   * @param msg error text for client describing how request is bad.
+   * @param cause cause of this exception.
+   */
+  public BadRequestException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
 }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 4775ac4..f8f6baa 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -25,6 +25,7 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Collections2;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
@@ -52,6 +53,7 @@
 import com.google.gerrit.server.index.change.ChangeField.PatchSetProtoField;
 import com.google.gerrit.server.index.change.ChangeIndex;
 import com.google.gerrit.server.index.change.ChangeIndexRewriter;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -127,6 +129,10 @@
   private static final String HASHTAG_FIELD =
       ChangeField.HASHTAG_CASE_AWARE.getName();
   private static final String STAR_FIELD = ChangeField.STAR.getName();
+  private static final String SUBMIT_RECORD_LENIENT_FIELD =
+      ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName();
+  private static final String SUBMIT_RECORD_STRICT_FIELD =
+      ChangeField.STORED_SUBMIT_RECORD_STRICT.getName();
 
   static Term idTerm(ChangeData cd) {
     return QueryBuilder.intTerm(LEGACY_ID.getName(), cd.getId().get());
@@ -465,6 +471,10 @@
     if (fields.contains(REVIEWER_FIELD)) {
       decodeReviewers(doc, cd);
     }
+    decodeSubmitRecords(doc, SUBMIT_RECORD_STRICT_FIELD,
+        ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd);
+    decodeSubmitRecords(doc, SUBMIT_RECORD_LENIENT_FIELD,
+        ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
     return cd;
   }
 
@@ -557,6 +567,14 @@
                 .transform(IndexableField::stringValue)));
   }
 
+  private void decodeSubmitRecords(Multimap<String, IndexableField> doc,
+      String field, SubmitRuleOptions opts, ChangeData cd) {
+    ChangeField.parseSubmitRecords(
+        Collections2.transform(
+            doc.get(field), f -> f.binaryValue().utf8ToString()),
+        opts, cd);
+  }
+
   private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc,
       String fieldName, ProtobufCodec<T> codec) {
     Collection<IndexableField> fields = doc.get(fieldName);
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
index 02827ab..35f79c9 100644
--- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -62,6 +62,7 @@
   private final OAuthTokenCache tokenCache;
   private OAuthServiceProvider serviceProvider;
   private OAuthUserInfo user;
+  private Account.Id accountId;
   private String redirectToken;
   private boolean linkMode;
 
@@ -80,7 +81,7 @@
   }
 
   boolean isLoggedIn() {
-    return tokenCache.has(user);
+    return user != null;
   }
 
   boolean isOAuthFinal(HttpServletRequest request) {
@@ -101,13 +102,10 @@
       OAuthToken token = oauth.getAccessToken(
           new OAuthVerifier(request.getParameter("code")));
       user = oauth.getUserInfo(token);
-      if (user != null && token != null) {
-        tokenCache.put(user, token);
-      }
 
       if (isLoggedIn()) {
         log.debug("Login-SUCCESS " + this);
-        authenticateAndRedirect(request, response);
+        authenticateAndRedirect(request, response, token);
         return true;
       }
       response.sendError(SC_UNAUTHORIZED);
@@ -128,7 +126,7 @@
   }
 
   private void authenticateAndRedirect(HttpServletRequest req,
-      HttpServletResponse rsp) throws IOException {
+      HttpServletResponse rsp, OAuthToken token) throws IOException {
     AuthRequest areq = new AuthRequest(user.getExternalId());
     AuthResult arsp;
     try {
@@ -147,6 +145,9 @@
       areq.setEmailAddress(user.getEmailAddress());
       areq.setDisplayName(user.getDisplayName());
       arsp = accountManager.authenticate(areq);
+
+      accountId = arsp.getAccountId();
+      tokenCache.put(accountId, token);
     } catch (AccountException e) {
       log.error("Unable to authenticate user \"" + user + "\"", e);
       rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
@@ -215,7 +216,10 @@
   }
 
   void logout() {
-    tokenCache.remove(user);
+    if (accountId != null) {
+      tokenCache.remove(accountId);
+      accountId = null;
+    }
     user = null;
     redirectToken = null;
     serviceProvider = null;
@@ -247,8 +251,8 @@
 
   @Override
   public String toString() {
-    return "OAuthSession [token=" + tokenCache.get(user) + ", user=" + user
-        + "]";
+    return "OAuthSession [token=" + tokenCache.get(accountId) + ", user="
+        + user + "]";
   }
 
   public void setServiceProvider(OAuthServiceProvider provider) {
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 c3d516b..3cf0549c 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
@@ -106,7 +106,9 @@
     extractMailExample("ChangeFooterHtml.soy");
     extractMailExample("ChangeSubject.soy");
     extractMailExample("Comment.soy");
+    extractMailExample("CommentHtml.soy");
     extractMailExample("CommentFooter.soy");
+    extractMailExample("CommentFooterHtml.soy");
     extractMailExample("DeleteReviewer.soy");
     extractMailExample("DeleteReviewerHtml.soy");
     extractMailExample("DeleteVote.soy");
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 7c69224..8baef83 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
@@ -309,6 +309,7 @@
     }
 
     byEmailCache.evict(account.getPreferredEmail());
+    byIdCache.evict(account.getId());
     realm.onCreateAccount(who, account);
     return new AuthResult(newId, extId.getKey(), true);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetOAuthToken.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetOAuthToken.java
index b6ba3bc..5d343c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetOAuthToken.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetOAuthToken.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -52,16 +53,13 @@
     if (self.get() != rsrc.getUser()) {
       throw new AuthException("not allowed to get access token");
     }
-    String username = rsrc.getUser().getAccount().getUserName();
-    if (username == null) {
-      throw new ResourceNotFoundException();
-    }
-    OAuthToken accessToken = tokenCache.get(username);
+    Account a = rsrc.getUser().getAccount();
+    OAuthToken accessToken = tokenCache.get(a.getId());
     if (accessToken == null) {
       throw new ResourceNotFoundException();
     }
     OAuthTokenInfo accessTokenInfo = new OAuthTokenInfo();
-    accessTokenInfo.username = username;
+    accessTokenInfo.username = a.getUserName();
     accessTokenInfo.resourceHost = hostName;
     accessTokenInfo.accessToken = accessToken.getToken();
     accessTokenInfo.providerId = accessToken.getProviderId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 7532a11..31c70d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -31,9 +31,9 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.Response;
@@ -43,6 +43,7 @@
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.Check;
+import com.google.gerrit.server.change.CreateMergePatchSet;
 import com.google.gerrit.server.change.DeleteAssignee;
 import com.google.gerrit.server.change.DeleteDraftChange;
 import com.google.gerrit.server.change.GetAssignee;
@@ -64,7 +65,6 @@
 import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.change.SubmittedTogether;
 import com.google.gerrit.server.change.SuggestChangeReviewers;
-import com.google.gerrit.server.change.CreateMergePatchSet;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
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 94a3ac2..69a1460 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
@@ -44,6 +44,9 @@
       @GerritServerConfig Config config) {
     this.loginProviders = loginProviders;
     this.editableAccountFields = new HashSet<>();
+    // User name should be always editable, because not all OAuth providers
+    // expose them
+    editableAccountFields.add(AccountFieldName.USER_NAME);
     if (config.getBoolean("oauth", null, "allowEditFullName", false)) {
       editableAccountFields.add(AccountFieldName.FULL_NAME);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index bcc5eab..94bdb06 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -19,8 +19,8 @@
 import com.google.common.cache.Cache;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.extensions.auth.oauth.OAuthTokenEncrypter;
-import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -37,54 +37,39 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        persist(OAUTH_TOKENS, String.class, OAuthToken.class);
+        persist(OAUTH_TOKENS, Account.Id.class, OAuthToken.class);
       }
     };
   }
 
-  private final Cache<String, OAuthToken> cache;
+  private final Cache<Account.Id, OAuthToken> cache;
 
   @Inject
-  OAuthTokenCache(@Named(OAUTH_TOKENS) Cache<String, OAuthToken> cache,
+  OAuthTokenCache(@Named(OAUTH_TOKENS) Cache<Account.Id, OAuthToken> cache,
       DynamicItem<OAuthTokenEncrypter> encrypter) {
     this.cache = cache;
     this.encrypter = encrypter;
   }
 
-  public boolean has(OAuthUserInfo user) {
-    return user != null
-      ? cache.getIfPresent(user.getUserName()) != null
-      : false;
-  }
-
-  public OAuthToken get(OAuthUserInfo user) {
-    return user != null
-      ? get(user.getUserName())
-      : null;
-  }
-
-  public OAuthToken get(String userName) {
-    OAuthToken accessToken = cache.getIfPresent(userName);
+  public OAuthToken get(Account.Id id) {
+    OAuthToken accessToken = cache.getIfPresent(id);
     if (accessToken == null) {
       return null;
     }
     accessToken = decrypt(accessToken);
     if (accessToken.isExpired()) {
-      cache.invalidate(userName);
+      cache.invalidate(id);
       return null;
     }
     return accessToken;
   }
 
-  public void put(OAuthUserInfo user, OAuthToken accessToken) {
-    cache.put(checkNotNull(user.getUserName()),
-        encrypt(checkNotNull(accessToken)));
+  public void put(Account.Id id, OAuthToken accessToken) {
+    cache.put(id, encrypt(checkNotNull(accessToken)));
   }
 
-  public void remove(OAuthUserInfo user) {
-    if (user != null) {
-      cache.invalidate(user.getUserName());
-    }
+  public void remove(Account.Id id) {
+    cache.invalidate(id);
   }
 
   private OAuthToken encrypt(OAuthToken token) {
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 771b049..6ea7eef 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
@@ -110,7 +110,7 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.QueryResult;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
@@ -143,6 +143,22 @@
 
 public class ChangeJson {
   private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
+
+  // Submit rule options in this class should always use fastEvalLabels for
+  // efficiency reasons. Callers that care about submittability after taking
+  // vote squashing into account should be looking at the submit action.
+  public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
+      ChangeField.SUBMIT_RULE_OPTIONS_LENIENT
+          .toBuilder()
+          .fastEvalLabels(true)
+          .build();
+
+  public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
+      ChangeField.SUBMIT_RULE_OPTIONS_STRICT
+          .toBuilder()
+          .fastEvalLabels(true)
+          .build();
+
   public static final Set<ListChangesOption> NO_OPTIONS =
       Collections.emptySet();
 
@@ -179,7 +195,6 @@
 
   private boolean lazyLoad = true;
   private AccountLoader accountLoader;
-  private Map<Change.Id, List<SubmitRecord>> submitRecords;
   private FixInput fix;
 
   @AssistedInject
@@ -555,34 +570,13 @@
   }
 
   private boolean submittable(ChangeData cd) throws OrmException {
-    List<SubmitRecord> records = new SubmitRuleEvaluator(cd)
-        .setFastEvalLabels(true)
-        .evaluate();
-    for (SubmitRecord sr : records) {
-      if (sr.status == SubmitRecord.Status.OK) {
-        return true;
-      }
-    }
-    return false;
+    return SubmitRecord.findOkRecord(
+            cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT))
+        .isPresent();
   }
 
   private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
-    // Maintain our own cache rather than using cd.getSubmitRecords(),
-    // since the latter may not have used the same values for
-    // fastEvalLabels/allowDraft/etc.
-    // TODO(dborowitz): Handle this better at the ChangeData level.
-    if (submitRecords == null) {
-      submitRecords = new HashMap<>();
-    }
-    List<SubmitRecord> records = submitRecords.get(cd.getId());
-    if (records == null) {
-      records = new SubmitRuleEvaluator(cd)
-        .setFastEvalLabels(true)
-        .setAllowDraft(true)
-        .evaluate();
-      submitRecords.put(cd.getId(), records);
-    }
-    return records;
+    return cd.submitRecords(SUBMIT_RULE_OPTIONS_LENIENT);
   }
 
   private Map<String, LabelInfo> labelsFor(ChangeControl ctl,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
index f117179..6eb144a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
@@ -20,8 +20,8 @@
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.extensions.common.MergePatchSetInput;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 49221c9..7457da5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -664,11 +664,9 @@
         op.updateRepo(ctx);
       }
 
-      if (!repoOnlyOps.isEmpty()) {
-        logDebug("Executing updateRepo on {} RepoOnlyOps", ops.size());
-        for (RepoOnlyOp op : repoOnlyOps) {
-          op.updateRepo(ctx);
-        }
+      logDebug("Executing updateRepo on {} RepoOnlyOps", repoOnlyOps.size());
+      for (RepoOnlyOp op : repoOnlyOps) {
+        op.updateRepo(ctx);
       }
 
       if (inserter != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
index 587ce09..880fc0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GroupList.java
@@ -34,12 +34,9 @@
 
   public static final String FILE_NAME = "groups";
 
-  private final Project.NameKey project;
   private final Map<AccountGroup.UUID, GroupReference> byUUID;
 
-  private GroupList(Project.NameKey project,
-      Map<AccountGroup.UUID, GroupReference> byUUID) {
-    this.project = project;
+  private GroupList(Map<AccountGroup.UUID, GroupReference> byUUID) {
     this.byUUID = byUUID;
   }
 
@@ -60,7 +57,7 @@
       groupsByUUID.put(uuid, ref);
     }
 
-    return new GroupList(project, groupsByUUID);
+    return new GroupList(groupsByUUID);
   }
 
   public GroupReference byUUID(AccountGroup.UUID uuid) {
@@ -70,7 +67,10 @@
   public GroupReference resolve(GroupReference group) {
     if (group != null) {
       if (group.getUUID() == null || group.getUUID().get() == null) {
-        log.warn("attempting to resolve null group in {}: {}", project, group);
+        // A GroupReference from ProjectConfig that refers to a group not found
+        // in this file will have a null UUID. Since there may be multiple
+        // different missing references, it's not appropriate to cache the
+        // results, nor return null the set from #uuids.
         return group;
       }
       GroupReference ref = byUUID.get(group.getUUID());
@@ -92,8 +92,7 @@
 
   public void put(AccountGroup.UUID uuid, GroupReference reference) {
     if (uuid == null || uuid.get() == null) {
-      log.warn("attempting to put null group in {}: {}", project, reference);
-      return;
+      return; // See note in #resolve above.
     }
     byUUID.put(uuid, reference);
   }
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 5aaf41a..005a91f 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
@@ -57,7 +57,7 @@
 import com.google.gerrit.server.git.validators.MergeValidators;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.util.RequestId;
@@ -81,7 +81,6 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -101,6 +100,9 @@
 public class MergeOp implements AutoCloseable {
   private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
 
+  private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS =
+      SubmitRuleOptions.defaults().build();
+
   public static class CommitStatus {
     private final ImmutableMap<Change.Id, ChangeData> changes;
     private final ImmutableSetMultimap<Branch.NameKey, Change.Id> byBranch;
@@ -173,7 +175,7 @@
       // However, do NOT expose that ChangeData directly, as it is way out of
       // date by this point.
       ChangeData cd = checkNotNull(changes.get(id), "ChangeData for %s", id);
-      return checkNotNull(cd.getSubmitRecords(),
+      return checkNotNull(cd.getSubmitRecords(SUBMIT_RULE_OPTIONS),
           "getSubmitRecord only valid after submit rules are evalutated");
     }
 
@@ -252,15 +254,6 @@
     orm.close();
   }
 
-  private static Optional<SubmitRecord> findOkRecord(
-      Collection<SubmitRecord> in) {
-    if (in == null) {
-      return Optional.empty();
-    }
-    return in.stream().filter(r -> r.status == SubmitRecord.Status.OK)
-        .findAny();
-  }
-
   public static void checkSubmitRule(ChangeData cd)
       throws ResourceConflictException, OrmException {
     PatchSet patchSet = cd.currentPatchSet();
@@ -269,7 +262,7 @@
           "missing current patch set for change " + cd.getId());
     }
     List<SubmitRecord> results = getSubmitRecords(cd);
-    if (findOkRecord(results).isPresent()) {
+    if (SubmitRecord.findOkRecord(results).isPresent()) {
       // Rules supplied a valid solution.
       return;
     } else if (results.isEmpty()) {
@@ -309,12 +302,7 @@
 
   private static List<SubmitRecord> getSubmitRecords(ChangeData cd)
       throws OrmException {
-    List<SubmitRecord> results = cd.getSubmitRecords();
-    if (results == null) {
-      results = new SubmitRuleEvaluator(cd).evaluate();
-      cd.setSubmitRecords(results);
-    }
-    return results;
+    return cd.submitRecords(SUBMIT_RULE_OPTIONS);
   }
 
   private static String describeLabels(ChangeData cd,
@@ -387,7 +375,7 @@
       SubmitRecord forced = new SubmitRecord();
       forced.status = SubmitRecord.Status.FORCED;
       records.add(forced);
-      cd.setSubmitRecords(records);
+      cd.setSubmitRecords(SUBMIT_RULE_OPTIONS, records);
     }
   }
 
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 741266d..337edbe 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
@@ -713,7 +713,7 @@
         // no valid UUID for it. Pool the reference anyway so at least
         // all rules in the same file share the same GroupReference.
         //
-        ref = resolve(rule.getGroup());
+        ref = rule.getGroup();
         groupsByName.put(ref.getName(), ref);
         error(new ValidationError(PROJECT_CONFIG,
             "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
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 1fdddca..74362b8 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
@@ -1838,10 +1838,9 @@
                   "commit(s) already exists (as current patchset)");
               newChanges = Collections.emptyList();
               return;
-            } else {
-              itr.remove();
-              continue;
             }
+            itr.remove();
+            continue;
           }
           newChangeIds.add(p.changeKey);
         }
@@ -1897,7 +1896,7 @@
   }
 
   private boolean foundInExistingRef(Collection<Ref> existingRefs)
-      throws OrmException, IOException {
+      throws OrmException {
     for (Ref ref : existingRefs) {
       ChangeNotes notes = notesFactory.create(db, project.getNameKey(),
           Change.Id.fromRef(ref.getName()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index 03f5df8..225b756 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.server.index.change;
 
 import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.common.base.Preconditions.checkArgument;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -27,21 +29,25 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Table;
+import com.google.gerrit.common.data.SubmitRecord;
 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.Comment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.SchemaUtil;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
+import com.google.gson.Gson;
 import com.google.gwtorm.protobuf.CodecFactory;
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
@@ -74,6 +80,8 @@
 public class ChangeField {
   public static final int NO_ASSIGNEE = -1;
 
+  private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
+
   /** Legacy change ID. */
   public static final FieldDef<ChangeData, Integer> LEGACY_ID =
       new FieldDef.Single<ChangeData, Integer>("legacy_id",
@@ -774,6 +782,169 @@
         }
       };
 
+  // Submit rule options in this class should never use fastEvalLabels. This
+  // slows down indexing slightly but produces correct search results.
+  public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
+      SubmitRuleOptions.defaults()
+          .allowClosed(true)
+          .allowDraft(true)
+          .build();
+
+  public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
+      SubmitRuleOptions.defaults().build();
+
+  /**
+   * JSON type for storing SubmitRecords.
+   * <p>
+   * Stored fields need to use a stable format over a long period; this type
+   * insulates the index from implementation changes in SubmitRecord itself.
+   */
+  static class StoredSubmitRecord {
+    static class StoredLabel {
+      String label;
+      SubmitRecord.Label.Status status;
+      Integer appliedBy;
+    }
+
+    SubmitRecord.Status status;
+    List<StoredLabel> labels;
+    String errorMessage;
+
+    StoredSubmitRecord(SubmitRecord rec) {
+      this.status = rec.status;
+      this.errorMessage = rec.errorMessage;
+      if (rec.labels != null) {
+        this.labels = new ArrayList<>(rec.labels.size());
+        for (SubmitRecord.Label label : rec.labels) {
+          StoredLabel sl = new StoredLabel();
+          sl.label = label.label;
+          sl.status = label.status;
+          sl.appliedBy =
+              label.appliedBy != null ? label.appliedBy.get() : null;
+          this.labels.add(sl);
+        }
+      }
+    }
+
+    private SubmitRecord toSubmitRecord() {
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = status;
+      rec.errorMessage = errorMessage;
+      if (labels != null) {
+        rec.labels = new ArrayList<>(labels.size());
+        for (StoredLabel label : labels) {
+          SubmitRecord.Label srl = new SubmitRecord.Label();
+          srl.label = label.label;
+          srl.status = label.status;
+          srl.appliedBy = label.appliedBy != null
+              ? new Account.Id(label.appliedBy)
+              : null;
+          rec.labels.add(srl);
+        }
+      }
+      return rec;
+    }
+  }
+
+  public static final FieldDef<ChangeData, Iterable<String>> SUBMIT_RECORD =
+      new FieldDef.Repeatable<ChangeData, String>(
+          "submit_record", FieldType.EXACT, false) {
+        @Override
+        public Iterable<String> get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return formatSubmitRecordValues(input);
+        }
+      };
+
+  public static final FieldDef<ChangeData, Iterable<byte[]>>
+      STORED_SUBMIT_RECORD_STRICT =
+          new FieldDef.Repeatable<ChangeData, byte[]>(
+              "full_submit_record_strict", FieldType.STORED_ONLY, true) {
+            @Override
+            public Iterable<byte[]> get(ChangeData input, FillArgs args)
+                throws OrmException {
+              return storedSubmitRecords(input, SUBMIT_RULE_OPTIONS_STRICT);
+            }
+          };
+
+  public static final FieldDef<ChangeData, Iterable<byte[]>>
+      STORED_SUBMIT_RECORD_LENIENT =
+          new FieldDef.Repeatable<ChangeData, byte[]>(
+              "full_submit_record_lenient", FieldType.STORED_ONLY, true) {
+            @Override
+            public Iterable<byte[]> get(ChangeData input, FillArgs args)
+                throws OrmException {
+              return storedSubmitRecords(input, SUBMIT_RULE_OPTIONS_LENIENT);
+            }
+          };
+
+  public static void parseSubmitRecords(
+      Collection<String> values, SubmitRuleOptions opts, ChangeData out) {
+    checkArgument(!opts.fastEvalLabels());
+    List<SubmitRecord> records = parseSubmitRecords(values);
+    if (records.isEmpty()) {
+      // Assume no values means the field is not in the index;
+      // SubmitRuleEvaluator ensures the list is non-empty.
+      return;
+    }
+    out.setSubmitRecords(opts, records);
+
+    // Cache the fastEvalLabels variant as well so it can be used by
+    // ChangeJson.
+    out.setSubmitRecords(
+        opts.toBuilder().fastEvalLabels(true).build(),
+        records);
+  }
+
+  @VisibleForTesting
+  static List<SubmitRecord> parseSubmitRecords(Collection<String> values) {
+    return values.stream()
+        .map(v -> GSON.fromJson(v, StoredSubmitRecord.class).toSubmitRecord())
+        .collect(toList());
+  }
+
+  @VisibleForTesting
+  static List<byte[]> storedSubmitRecords(List<SubmitRecord> records) {
+    return Lists.transform(
+        records, r -> GSON.toJson(new StoredSubmitRecord(r)).getBytes(UTF_8));
+  }
+
+  private static Iterable<byte[]> storedSubmitRecords(
+      ChangeData cd, SubmitRuleOptions opts) throws OrmException {
+    return storedSubmitRecords(cd.submitRecords(opts));
+  }
+
+  public static List<String> formatSubmitRecordValues(ChangeData cd)
+      throws OrmException {
+    return formatSubmitRecordValues(
+        cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT),
+        cd.change().getOwner());
+  }
+
+  @VisibleForTesting
+  static List<String> formatSubmitRecordValues(List<SubmitRecord> records,
+      Account.Id changeOwner) {
+    List<String> result = new ArrayList<>();
+    for (SubmitRecord rec : records) {
+      result.add(rec.status.name());
+      if (rec.labels == null) {
+        continue;
+      }
+      for (SubmitRecord.Label label : rec.labels) {
+        String sl = label.status.toString() + ',' + label.label.toLowerCase();
+        result.add(sl);
+        String slc = sl + ',';
+        if (label.appliedBy != null) {
+          result.add(slc + label.appliedBy.get());
+          if (label.appliedBy.equals(changeOwner)) {
+            result.add(slc + ChangeQueryBuilder.OWNER_ACCOUNT_ID.get());
+          }
+        }
+      }
+    }
+    return result;
+  }
+
   public static final Integer NOT_REVIEWED = -1;
 
   private static String getTopic(ChangeData input) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 3c0ff0c..8a793e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -66,13 +66,19 @@
   static final Schema<ChangeData> V33 =
       schema(V32, ChangeField.ASSIGNEE);
 
-  @SuppressWarnings("deprecation")
+  @Deprecated
   static final Schema<ChangeData> V34 = new Schema.Builder<ChangeData>()
       .add(V33)
       .remove(ChangeField.LABEL)
       .add(ChangeField.LABEL2)
       .build();
 
+  static final Schema<ChangeData> V35 =
+      schema(V34,
+          ChangeField.SUBMIT_RECORD,
+          ChangeField.STORED_SUBMIT_RECORD_LENIENT,
+          ChangeField.STORED_SUBMIT_RECORD_STRICT);
+
   public static final String NAME = "changes";
   public static final ChangeSchemaDefinitions INSTANCE =
       new ChangeSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index 70a4888..d6d1319 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
@@ -39,9 +39,12 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 
@@ -55,6 +58,44 @@
     CommentSender create(Project.NameKey project, Change.Id id);
   }
 
+  private class FileCommentGroup {
+    public String filename;
+    public int patchSetId;
+    public PatchFile fileData;
+    public List<Comment> comments = new ArrayList<>();
+
+    /**
+     * @return a web link to the given patch set and file.
+     */
+    public String getLink() {
+      String url = getGerritUrl();
+      if (url == null) {
+        return null;
+      }
+
+      return new StringBuilder()
+        .append(url)
+        .append("#/c/").append(change.getId())
+        .append('/').append(patchSetId)
+        .append('/').append(KeyUtil.encode(filename))
+        .toString();
+    }
+
+    /**
+     * @return A title for the group, i.e. "Commit Message", "Merge List", or
+     * "File [[filename]]".
+     */
+    public String getTitle() {
+      if (Patch.COMMIT_MSG.equals(filename)) {
+        return "Commit Message";
+      } else if (Patch.MERGE_LIST.equals(filename)) {
+        return "Merge List";
+      } else {
+        return "File " + filename;
+      }
+    }
+  }
+
   private List<Comment> inlineComments = Collections.emptyList();
   private final CommentsUtil commentsUtil;
 
@@ -95,24 +136,66 @@
   @Override
   public void formatChange() throws EmailException {
     appendText(textTemplate("Comment"));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("CommentHtml"));
+    }
   }
 
   @Override
   public void formatFooter() throws EmailException {
     appendText(textTemplate("CommentFooter"));
+    if (useHtml()) {
+      appendHtml(soyHtmlTemplate("CommentFooterHtml"));
+    }
   }
 
+  /**
+   * No longer used outside Velocity. Remove this method when VTL support is
+   * removed.
+   */
+  @Deprecated
   public boolean hasInlineComments() {
     return !inlineComments.isEmpty();
   }
 
+  /**
+   * No longer used outside Velocity. Remove this method when VTL support is
+   * removed.
+   */
+  @Deprecated
   public String getInlineComments() {
     return getInlineComments(1);
   }
 
+  /**
+   * No longer used outside Velocity. Remove this method when VTL support is
+   * removed.
+   */
+  @Deprecated
   public String getInlineComments(int lines) {
     StringBuilder cmts = new StringBuilder();
+    for (FileCommentGroup group : getGroupedInlineComments()) {
+      String link = group.getLink();
+      if (link != null) {
+        cmts.append(link).append('\n');
+      }
+      cmts.append(group.getTitle()).append(":\n\n");
+      for (Comment c : group.comments) {
+        appendComment(cmts, lines, group.fileData, c);
+      }
+      cmts.append("\n\n");
+    }
+    return cmts.toString();
+  }
+
+  /**
+   * @return a list of FileCommentGroup objects representing the inline comments
+   * grouped by the file.
+   */
+  private List<CommentSender.FileCommentGroup> getGroupedInlineComments() {
+    List<CommentSender.FileCommentGroup> groups = new ArrayList<>();
     try (Repository repo = getRepository()) {
+      // Get the patch list:
       PatchList patchList = null;
       if (repo != null) {
         try {
@@ -122,29 +205,21 @@
         }
       }
 
-      String currentFileName = null;
-      int currentPatchSetId = -1;
-      PatchFile currentFileData = null;
+      // Loop over the comments and collect them into groups based on the file
+      // location of the comment.
+      FileCommentGroup currentGroup = null;
       for (Comment c : inlineComments) {
-        if (!c.key.filename.equals(currentFileName)
-            || c.key.patchSetId != currentPatchSetId) {
-          String link = makeLink(change.getId(), c.key);
-          if (link != null) {
-            cmts.append(link).append('\n');
-          }
-          if (Patch.COMMIT_MSG.equals(c.key.filename)) {
-            cmts.append("Commit Message:\n\n");
-          } else if (Patch.MERGE_LIST.equals(c.key.filename)) {
-            cmts.append("Merge List:\n\n");
-          } else {
-            cmts.append("File ").append(c.key.filename).append(":\n\n");
-          }
-          currentFileName = c.key.filename;
-          currentPatchSetId = c.key.patchSetId;
-
+        // If it's a new group:
+        if (currentGroup == null
+            || !c.key.filename.equals(currentGroup.filename)
+            || c.key.patchSetId != currentGroup.patchSetId) {
+          currentGroup = new FileCommentGroup();
+          currentGroup.filename = c.key.filename;
+          currentGroup.patchSetId = c.key.patchSetId;
+          groups.add(currentGroup);
           if (patchList != null) {
             try {
-              currentFileData =
+              currentGroup.fileData =
                   new PatchFile(repo, patchList, c.key.filename);
             } catch (IOException e) {
               log.warn(String.format(
@@ -152,20 +227,25 @@
                   c.key.filename,
                   patchList.getNewId().name(),
                   projectState.getProject().getName()), e);
-              currentFileData = null;
+              currentGroup.fileData = null;
             }
           }
         }
 
-        if (currentFileData != null) {
-          appendComment(cmts, lines, currentFileData, c);
+        if (currentGroup.fileData != null) {
+          currentGroup.comments.add(c);
         }
-        cmts.append("\n\n");
       }
     }
-    return cmts.toString();
+
+    return groups;
   }
 
+  /**
+   * No longer used except for Velocity. Remove this method when VTL support is
+   * removed.
+   */
+  @Deprecated
   private void appendComment(StringBuilder out, int contextLines,
       PatchFile currentFileData, Comment comment) {
     if (comment instanceof RobotComment) {
@@ -176,62 +256,107 @@
          .append(robotComment.robotRunId)
          .append("):\n");
     }
-    short side = comment.side;
-    Comment.Range range = comment.range;
-    if (range != null) {
-      String prefix = "PS" + comment.key.patchSetId
-        + ", Line " + range.startLine + ": ";
-      for (int n = range.startLine; n <= range.endLine; n++) {
-        out.append(n == range.startLine
-            ? prefix
-            : Strings.padStart(": ", prefix.length(), ' '));
-        String s = getLine(currentFileData, side, n);
-        if (n == range.startLine && n == range.endLine) {
-          s = s.substring(
-              Math.min(range.startChar, s.length()),
-              Math.min(range.endChar, s.length()));
-        } else if (n == range.startLine) {
-          s = s.substring(Math.min(range.startChar, s.length()));
-        } else if (n == range.endLine) {
-          s = s.substring(0, Math.min(range.endChar, s.length()));
-        }
-        out.append(s).append('\n');
-      }
-      appendQuotedParent(out, comment);
-      out.append(comment.message.trim()).append('\n');
+    if (comment.range != null) {
+      appendRangedComment(out, currentFileData, comment);
     } else {
-      int lineNbr = comment.lineNbr;
-
-      // Initialize maxLines to the known line number.
-      int maxLines = lineNbr;
-
-      try {
-        maxLines = currentFileData.getLineCount(side);
-      } catch (IOException err) {
-        // The file could not be read, leave the max as is.
-        log.warn(String.format("Failed to read file %s on side %d",
-            comment.key.filename, side), err);
-      } catch (NoSuchEntityException err) {
-        // The file could not be read, leave the max as is.
-        log.warn(String.format("Side %d of file %s didn't exist",
-             side, comment.key.filename), err);
-      }
-
-      final int startLine = Math.max(1, lineNbr - contextLines + 1);
-      final int stopLine = Math.min(maxLines, lineNbr + contextLines);
-
-      for (int line = startLine; line <= lineNbr; ++line) {
-        appendFileLine(out, currentFileData, side, line);
-      }
-      appendQuotedParent(out, comment);
-      out.append(comment.message.trim()).append('\n');
-
-      for (int line = lineNbr + 1; line < stopLine; ++line) {
-        appendFileLine(out, currentFileData, side, line);
-      }
+      appendLineComment(out, contextLines, currentFileData, comment);
     }
   }
 
+  /**
+   * No longer used except for Velocity. Remove this method when VTL support is
+   * removed.
+   */
+  @Deprecated
+  private void appendRangedComment(StringBuilder out, PatchFile fileData,
+      Comment comment) {
+    String prefix = getCommentLinePrefix(comment);
+    String emptyPrefix = Strings.padStart(": ", prefix.length(), ' ');
+    boolean firstLine = true;
+    for (String line : getLinesByRange(comment.range, fileData, comment.side)) {
+      out.append(firstLine ? prefix : emptyPrefix)
+          .append(line)
+          .append('\n');
+      firstLine = false;
+    }
+    appendQuotedParent(out, comment);
+    out.append(comment.message.trim()).append('\n');
+  }
+
+  private String getCommentLinePrefix(Comment comment) {
+    int lineNbr = comment.range == null ?
+        comment.lineNbr : comment.range.startLine;
+    return "PS" + comment.key.patchSetId + ", Line " + lineNbr + ": ";
+  }
+
+  /**
+   * @return the lines of file content in fileData that are encompassed by range
+   * on the given side.
+   */
+  private List<String> getLinesByRange(Comment.Range range,
+      PatchFile fileData, short side) {
+    List<String> lines = new ArrayList<>();
+
+    for (int n = range.startLine; n <= range.endLine; n++) {
+      String s = getLine(fileData, side, n);
+      if (n == range.startLine && n == range.endLine) {
+        s = s.substring(
+            Math.min(range.startChar, s.length()),
+            Math.min(range.endChar, s.length()));
+      } else if (n == range.startLine) {
+        s = s.substring(Math.min(range.startChar, s.length()));
+      } else if (n == range.endLine) {
+        s = s.substring(0, Math.min(range.endChar, s.length()));
+      }
+      lines.add(s);
+    }
+    return lines;
+  }
+
+  /**
+   * No longer used except for Velocity. Remove this method when VTL support is
+   * removed.
+   */
+  @Deprecated
+  private void appendLineComment(StringBuilder out, int contextLines,
+      PatchFile currentFileData, Comment comment) {
+    short side = comment.side;
+    int lineNbr = comment.lineNbr;
+
+    // Initialize maxLines to the known line number.
+    int maxLines = lineNbr;
+
+    try {
+      maxLines = currentFileData.getLineCount(side);
+    } catch (IOException err) {
+      // The file could not be read, leave the max as is.
+      log.warn(String.format("Failed to read file %s on side %d",
+          comment.key.filename, side), err);
+    } catch (NoSuchEntityException err) {
+      // The file could not be read, leave the max as is.
+      log.warn(String.format("Side %d of file %s didn't exist",
+           side, comment.key.filename), err);
+    }
+
+    int startLine = Math.max(1, lineNbr - contextLines + 1);
+    int stopLine = Math.min(maxLines, lineNbr + contextLines);
+
+    for (int line = startLine; line <= lineNbr; ++line) {
+      appendFileLine(out, currentFileData, side, line);
+    }
+    appendQuotedParent(out, comment);
+    out.append(comment.message.trim()).append('\n');
+
+    for (int line = lineNbr + 1; line < stopLine; ++line) {
+      appendFileLine(out, currentFileData, side, line);
+    }
+  }
+
+  /**
+   * No longer used except for Velocity. Remove this method when VTL support is
+   * removed.
+   */
+  @Deprecated
   private void appendFileLine(StringBuilder cmts, PatchFile fileData,
       short side, int line) {
     String lineStr = getLine(fileData, side, line);
@@ -242,45 +367,131 @@
         .append("\n");
   }
 
+  /**
+   * No longer used except for Velocity. Remove this method when VTL support is
+   * removed.
+   */
+  @Deprecated
   private void appendQuotedParent(StringBuilder out, Comment child) {
-    if (child.parentUuid != null) {
-      Optional<Comment> parent;
-      Comment.Key key = new Comment.Key(child.parentUuid, child.key.filename,
-          child.key.patchSetId);
-      try {
-        parent = commentsUtil.get(args.db.get(), changeData.notes(), key);
-      } catch (OrmException e) {
-        log.warn("Could not find the parent of this comment: "
-            + child.toString());
-        parent = Optional.empty();
-      }
-      if (parent.isPresent()) {
-        String msg = parent.get().message.trim();
-        if (msg.length() > 75) {
-          msg = msg.substring(0, 75);
-        }
-        int lf = msg.indexOf('\n');
-        if (lf > 0) {
-          msg = msg.substring(0, lf);
-        }
-        out.append("> ").append(msg).append('\n');
-      }
+    Optional<Comment> parent = getParent(child);
+    if (parent.isPresent()) {
+      out.append("> ")
+          .append(getShortenedCommentMessage(parent.get()))
+          .append('\n');
     }
   }
 
-  // Makes a link back to the given patch set and file.
-  private String makeLink(Change.Id changeId, Comment.Key key) {
-    String url = getGerritUrl();
-    if (url == null) {
-      return null;
+  /**
+   * Get the parent comment of a given comment.
+   * @param child the comment with a potential parent comment.
+   * @return an optional comment that will be  present if the given comment has
+   * a parent, and is empty if it does not.
+   */
+  private Optional<Comment> getParent(Comment child) {
+    if (child.parentUuid == null) {
+      return Optional.empty();
     }
 
-    return new StringBuilder()
-      .append(url)
-      .append("#/c/").append(changeId)
-      .append('/').append(key.patchSetId)
-      .append('/').append(KeyUtil.encode(key.filename))
-      .toString();
+    Comment.Key key = new Comment.Key(child.parentUuid, child.key.filename,
+          child.key.patchSetId);
+    try {
+      return commentsUtil.get(args.db.get(), changeData.notes(), key);
+    } catch (OrmException e) {
+      log.warn("Could not find the parent of this comment: "
+          + child.toString());
+      return Optional.empty();
+    }
+  }
+
+  /**
+   * Retrieve the file lines refered to by a comment.
+   * @param comment The comment that refers to some file contents. The comment
+   *     may be a line comment or a ranged comment.
+   * @param fileData The file on which the comment appears.
+   * @return file contents referred to by the comment. If the comment is a line
+   *     comment, the result will be a list of one string. Otherwise it will be
+   *     a list of one or more strings.
+   */
+  private List<String> getLinesOfComment(Comment comment, PatchFile fileData) {
+    List<String> lines = new ArrayList<>();
+    if (comment.range == null) {
+      lines.add(getLine(fileData, comment.side, comment.lineNbr));
+    } else {
+      lines.addAll(getLinesByRange(comment.range, fileData, comment.side));
+    }
+    return lines;
+  }
+
+  /**
+   * @return a shortened version of the given comment's message. Will be
+   * shortened to 75 characters or the first line, whichever is shorter.
+   */
+  private String getShortenedCommentMessage(Comment comment) {
+    String msg = comment.message.trim();
+    if (msg.length() > 75) {
+      msg = msg.substring(0, 75);
+    }
+    int lf = msg.indexOf('\n');
+    if (lf > 0) {
+      msg = msg.substring(0, lf);
+    }
+    return msg;
+  }
+
+  /**
+   * @return grouped inline comment data mapped to data structures that are
+   * suitable for passing into Soy.
+   */
+  private List<Map<String, Object>> getCommentGroupsTemplateData() {
+    List<Map<String, Object>> commentGroups = new ArrayList<>();
+
+    for (CommentSender.FileCommentGroup group : getGroupedInlineComments()) {
+      Map<String, Object> groupData = new HashMap<>();
+      groupData.put("link", group.getLink());
+      groupData.put("title", group.getTitle());
+      groupData.put("patchSetId", group.patchSetId);
+
+      List<Map<String, Object>> commentsList = new ArrayList<>();
+      for (Comment comment : group.comments) {
+        Map<String, Object> commentData = new HashMap<>();
+        commentData.put("lines", getLinesOfComment(comment, group.fileData));
+        commentData.put("message", comment.message.trim());
+
+        String prefix = getCommentLinePrefix(comment);
+        commentData.put("linePrefix", prefix);
+        commentData.put("linePrefixEmpty",
+            Strings.padStart(": ", prefix.length(), ' '));
+
+        if (comment.range == null) {
+          commentData.put("startLine", comment.lineNbr);
+        } else {
+          commentData.put("startLine", comment.range.startLine);
+          commentData.put("endLine", comment.range.endLine);
+        }
+
+        if (comment instanceof RobotComment) {
+          RobotComment robotComment = (RobotComment) comment;
+          commentData.put("isRobotComment", true);
+          commentData.put("robotId", robotComment.robotId);
+          commentData.put("robotRunId", robotComment.robotRunId);
+          commentData.put("robotUrl", robotComment.url);
+        } else {
+          commentData.put("isRobotComment", false);
+        }
+
+        Optional<Comment> parent = getParent(comment);
+        if (parent.isPresent()) {
+          commentData.put("parentMessage",
+              getShortenedCommentMessage(parent.get()));
+        }
+
+        commentsList.add(commentData);
+      }
+      groupData.put("comments", commentsList);
+
+      commentGroups.add(groupData);
+    }
+    return commentGroups;
   }
 
   private Repository getRepository() {
@@ -294,8 +505,7 @@
   @Override
   protected void setupSoyContext() {
     super.setupSoyContext();
-    soyContextEmailData.put("inlineComments", getInlineComments());
-    soyContextEmailData.put("hasInlineComments", hasInlineComments());
+    soyContext.put("commentFiles", getCommentGroupsTemplateData());
   }
 
   private String getLine(PatchFile fileInfo, short side, int lineNbr) {
@@ -317,4 +527,9 @@
       return "";
     }
   }
+
+  @Override
+  protected boolean supportsHtml() {
+    return true;
+  }
 }
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
index 26bd99e..2381c16 100644
--- 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
@@ -44,7 +44,9 @@
     "ChangeFooterHtml.soy",
     "ChangeSubject.soy",
     "Comment.soy",
+    "CommentHtml.soy",
     "CommentFooter.soy",
+    "CommentFooterHtml.soy",
     "DeleteReviewer.soy",
     "DeleteReviewerHtml.soy",
     "DeleteVote.soy",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index d27d4f9..6f85a28 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.project;
 
+import static java.util.stream.Collectors.toSet;
+
 import com.google.common.base.Throwables;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
@@ -40,9 +42,9 @@
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.Objects;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.concurrent.ExecutionException;
@@ -200,25 +202,24 @@
   }
 
   @Override
-  public Iterable<Project.NameKey> all() {
+  public SortedSet<Project.NameKey> all() {
     try {
       return list.get(ListKey.ALL);
     } catch (ExecutionException e) {
       log.warn("Cannot list available projects", e);
-      return Collections.emptyList();
+      return Collections.emptySortedSet();
     }
   }
 
   @Override
   public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
-    Set<AccountGroup.UUID> groups = new HashSet<>();
-    for (Project.NameKey n : all()) {
-      ProjectState p = byName.getIfPresent(n.get());
-      if (p != null) {
-        groups.addAll(p.getConfig().getAllGroupUUIDs());
-      }
-    }
-    return groups;
+    return all().stream().map(n -> byName.getIfPresent(n.get()))
+        .filter(Objects::nonNull)
+        .flatMap(p -> p.getConfig().getAllGroupUUIDs().stream())
+        // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
+        // against them just in case there is a bug or corner case.
+        .filter(id -> id != null && id.get() != null)
+        .collect(toSet());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index b0c521b..9a5b61f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -91,12 +91,9 @@
   private final ChangeData cd;
   private final ChangeControl control;
 
+  private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.defaults();
+  private SubmitRuleOptions opts;
   private PatchSet patchSet;
-  private boolean fastEvalLabels;
-  private boolean allowDraft;
-  private boolean allowClosed;
-  private boolean skipFilters;
-  private String rule;
   private boolean logErrors = true;
   private long reductionsConsumed;
 
@@ -108,6 +105,29 @@
   }
 
   /**
+   * @return immutable snapshot of options configured so far. If neither {@link
+   *     #getSubmitRule()} nor {@link #getSubmitType()} have been called yet,
+   *     state within this instance is still mutable, so may change before
+   *     evaluation. The instance's options are frozen at evaluation time.
+   */
+  public SubmitRuleOptions getOptions() {
+    if (opts != null) {
+      return opts;
+    }
+    return optsBuilder.build();
+  }
+
+  public SubmitRuleEvaluator setOptions(SubmitRuleOptions opts) {
+    checkNotStarted();
+    if (opts != null) {
+      optsBuilder = opts.toBuilder();
+    } else {
+      optsBuilder = SubmitRuleOptions.defaults();
+    }
+    return this;
+  }
+
+  /**
    * @param ps patch set of the change to evaluate. If not set, the current
    * patch set will be loaded from {@link #evaluate()} or {@link
    * #getSubmitType}.
@@ -127,7 +147,8 @@
    * @return this
    */
   public SubmitRuleEvaluator setFastEvalLabels(boolean fast) {
-    fastEvalLabels = fast;
+    checkNotStarted();
+    optsBuilder.fastEvalLabels(fast);
     return this;
   }
 
@@ -136,7 +157,8 @@
    * @return this
    */
   public SubmitRuleEvaluator setAllowClosed(boolean allow) {
-    allowClosed = allow;
+    checkNotStarted();
+    optsBuilder.allowClosed(allow);
     return this;
   }
 
@@ -145,7 +167,8 @@
    * @return this
    */
   public SubmitRuleEvaluator setAllowDraft(boolean allow) {
-    allowDraft = allow;
+    checkNotStarted();
+    optsBuilder.allowDraft(allow);
     return this;
   }
 
@@ -154,7 +177,8 @@
    * @return this
    */
   public SubmitRuleEvaluator setSkipSubmitFilters(boolean skip) {
-    skipFilters = skip;
+    checkNotStarted();
+    optsBuilder.skipFilters(skip);
     return this;
   }
 
@@ -163,7 +187,8 @@
    * @return this
    */
   public SubmitRuleEvaluator setRule(@Nullable String rule) {
-    this.rule = rule;
+    checkNotStarted();
+    optsBuilder.rule(rule);
     return this;
   }
 
@@ -188,13 +213,14 @@
    *     rules, including any errors.
    */
   public List<SubmitRecord> evaluate() {
+    initOptions();
     Change c = control.getChange();
-    if (!allowClosed && c.getStatus().isClosed()) {
+    if (!opts.allowClosed() && c.getStatus().isClosed()) {
       SubmitRecord rec = new SubmitRecord();
       rec.status = SubmitRecord.Status.CLOSED;
       return Collections.singletonList(rec);
     }
-    if (!allowDraft) {
+    if (!opts.allowDraft()) {
       if (c.getStatus() == Change.Status.DRAFT) {
         return cannotSubmitDraft();
       }
@@ -242,7 +268,10 @@
       }
       return createRuleError("Cannot submit draft changes");
     } catch (OrmException err) {
-      String msg = "Cannot check visibility of patch set " + patchSet.getId();
+      PatchSet.Id psId = patchSet != null
+          ? patchSet.getId()
+          : control.getChange().currentPatchSetId();
+      String msg = "Cannot check visibility of patch set " + psId;
       log.error(msg, err);
       return createRuleError(msg);
     }
@@ -369,6 +398,7 @@
    * @return record from the evaluated rules.
    */
   public SubmitTypeRecord getSubmitType() {
+    initOptions();
     try {
       initPatchSet();
     } catch (OrmException e) {
@@ -453,7 +483,7 @@
     PrologEnvironment env = getPrologEnvironment(user);
     try {
       Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
-      if (fastEvalLabels) {
+      if (opts.fastEvalLabels()) {
         env.once("gerrit", "assume_range_from_label");
       }
 
@@ -476,7 +506,7 @@
       }
 
       Term resultsTerm = toListTerm(results);
-      if (!skipFilters) {
+      if (!opts.skipFilters()) {
         resultsTerm = runSubmitFilters(
             resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
       }
@@ -503,18 +533,19 @@
     ProjectState projectState = control.getProjectControl().getProjectState();
     PrologEnvironment env;
     try {
-      if (rule == null) {
+      if (opts.rule() == null) {
         env = projectState.newPrologEnvironment();
       } else {
-        env = projectState.newPrologEnvironment("stdin", new StringReader(rule));
+        env = projectState.newPrologEnvironment(
+            "stdin", new StringReader(opts.rule()));
       }
     } catch (CompileException err) {
       String msg;
-      if (rule == null && control.getProjectControl().isOwner()) {
+      if (opts.rule() == null && control.getProjectControl().isOwner()) {
         msg = String.format(
             "Cannot load rules.pl for %s: %s",
             getProjectName(), err.getMessage());
-      } else if (rule != null) {
+      } else if (opts.rule() != null) {
         msg = err.getMessage();
       } else {
         msg = String.format("Cannot load rules.pl for %s", getProjectName());
@@ -548,7 +579,7 @@
       Term filterRule =
           parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
       try {
-        if (fastEvalLabels) {
+        if (opts.fastEvalLabels()) {
           env.once("gerrit", "assume_range_from_label");
         }
 
@@ -608,6 +639,17 @@
     return submitRule != null ? submitRule.toString() : "<unknown rule>";
   }
 
+  private void checkNotStarted() {
+    checkState(opts == null, "cannot set options after starting evaluation");
+  }
+
+  private void initOptions() {
+    if (opts == null) {
+      opts = optsBuilder.build();
+      optsBuilder = null;
+    }
+  }
+
   private void initPatchSet() throws OrmException {
     if (patchSet == null) {
       patchSet = cd.currentPatchSet();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleOptions.java
new file mode 100644
index 0000000..97155ac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleOptions.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+
+/**
+ * Stable identifier for options passed to a particular submit rule evaluator.
+ * <p>
+ * Used to test whether it is ok to reuse a cached list of submit records. Does
+ * not include a change or patch set ID; callers are responsible for checking
+ * those on their own.
+ */
+@AutoValue
+public abstract class SubmitRuleOptions {
+  public static Builder builder() {
+    return new AutoValue_SubmitRuleOptions.Builder();
+  }
+
+  public static Builder defaults() {
+    return builder()
+        .fastEvalLabels(false)
+        .allowDraft(false)
+        .allowClosed(false)
+        .skipFilters(false)
+        .rule(null);
+  }
+
+  public abstract boolean fastEvalLabels();
+  public abstract boolean allowDraft();
+  public abstract boolean allowClosed();
+  public abstract boolean skipFilters();
+  @Nullable public abstract String rule();
+
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract SubmitRuleOptions.Builder fastEvalLabels(boolean fastEvalLabels);
+    public abstract SubmitRuleOptions.Builder allowDraft(boolean allowDraft);
+    public abstract SubmitRuleOptions.Builder allowClosed(boolean allowClosed);
+    public abstract SubmitRuleOptions.Builder skipFilters(boolean skipFilters);
+    public abstract SubmitRuleOptions.Builder rule(@Nullable String rule);
+
+    public abstract SubmitRuleOptions build();
+  }
+
+  public Builder toBuilder() {
+    return builder()
+        .fastEvalLabels(fastEvalLabels())
+        .allowDraft(allowDraft())
+        .allowClosed(allowClosed())
+        .skipFilters(skipFilters())
+        .rule(rule());
+  }
+}
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 4d55c80..eaec021 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
@@ -65,6 +65,7 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.project.SubmitRuleOptions;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.assistedinject.Assisted;
@@ -325,6 +326,9 @@
   private final MergeabilityCache mergeabilityCache;
   private final StarredChangesUtil starredChangesUtil;
   private final Change.Id legacyId;
+  private final Map<SubmitRuleOptions, List<SubmitRecord>>
+      submitRecords = Maps.newLinkedHashMapWithExpectedSize(1);
+
   private Project.NameKey project;
   private Change change;
   private ChangeNotes notes;
@@ -341,7 +345,6 @@
   private CurrentUser visibleTo;
   private ChangeControl changeControl;
   private List<ChangeMessage> messages;
-  private List<SubmitRecord> submitRecords;
   private Optional<ChangedLines> changedLines;
   private SubmitTypeRecord submitTypeRecord;
   private Boolean mergeable;
@@ -1011,12 +1014,30 @@
     return messages;
   }
 
-  public void setSubmitRecords(List<SubmitRecord> records) {
-    submitRecords = records;
+  public List<SubmitRecord> submitRecords(
+      SubmitRuleOptions options) throws OrmException {
+    List<SubmitRecord> records = submitRecords.get(options);
+    if (records == null) {
+      if (!lazyLoad) {
+        return Collections.emptyList();
+      }
+      records = new SubmitRuleEvaluator(this)
+          .setOptions(options)
+          .evaluate();
+      submitRecords.put(options, records);
+    }
+    return records;
   }
 
-  public List<SubmitRecord> getSubmitRecords() {
-    return submitRecords;
+  @Nullable
+  public List<SubmitRecord> getSubmitRecords(
+      SubmitRuleOptions options) {
+    return submitRecords.get(options);
+  }
+
+  public void setSubmitRecords(SubmitRuleOptions options,
+      List<SubmitRecord> records) {
+    submitRecords.put(options, records);
   }
 
   public SubmitTypeRecord submitTypeRecord() throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 390f8b8..f9d4f68 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -19,11 +19,13 @@
 import static java.util.stream.Collectors.toSet;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Enums;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.reviewdb.client.Account;
@@ -488,6 +490,10 @@
       return new AssigneePredicate(new Account.Id(ChangeField.NO_ASSIGNEE));
     }
 
+    if ("submittable".equalsIgnoreCase(value)) {
+      return new SubmittablePredicate(SubmitRecord.Status.OK);
+    }
+
     try {
       return status(value);
     } catch (IllegalArgumentException e) {
@@ -644,7 +650,7 @@
           try {
             group = parseGroup(value).getUUID();
           } catch (QueryParseException e) {
-            throw error("Neither user nor group " + value + " found");
+            throw error("Neither user nor group " + value + " found", e);
           }
         }
       }
@@ -665,9 +671,35 @@
       }
     }
 
+    // If the vote piece looks like Code-Review=NEED with a valid non-numeric
+    // submit record status, interpret as a submit record query.
+    int eq = name.indexOf('=');
+    if (args.getSchema().hasField(ChangeField.SUBMIT_RECORD) && eq > 0) {
+      String statusName = name.substring(eq + 1).toUpperCase();
+      if (!isInt(statusName)) {
+        SubmitRecord.Label.Status status = Enums.getIfPresent(
+            SubmitRecord.Label.Status.class, statusName).orNull();
+        if (status == null) {
+          throw error("Invalid label status " + statusName + " in " + name);
+        }
+        return SubmitRecordPredicate.create(
+            name.substring(0, eq), status, accounts);
+      }
+    }
+
     return new LabelPredicate(args, name, accounts, group);
   }
 
+  private static boolean isInt(String s) {
+    if (s == null) {
+      return false;
+    }
+    if (s.startsWith("+")) {
+      s = s.substring(1);
+    }
+    return Ints.tryParse(s) != null;
+  }
+
   @Operator
   public Predicate<ChangeData> message(String text) {
     return new MessagePredicate(args.index, text);
@@ -964,6 +996,17 @@
     return new CommitterPredicate(who);
   }
 
+  @Operator
+  public Predicate<ChangeData> submittable(String str)
+      throws QueryParseException {
+    SubmitRecord.Status status = Enums.getIfPresent(
+        SubmitRecord.Status.class, str.toUpperCase()).orNull();
+    if (status == null) {
+      throw error("invalid value for submittable:" + str);
+    }
+    return new SubmittablePredicate(status);
+  }
+
   @Override
   protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
     if (query.startsWith("refs/")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index ad32edd..7b8f5fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -103,7 +103,7 @@
         String op = m.group(1);
         throw new AuthException("Must be signed-in to use " + op);
       }
-      throw new BadRequestException(e.getMessage());
+      throw new BadRequestException(e.getMessage(), e);
     }
     return out.size() == 1 ? out.get(0) : out;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
new file mode 100644
index 0000000..ec3c56f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.Set;
+
+class SubmitRecordPredicate extends ChangeIndexPredicate {
+  static Predicate<ChangeData> create(String label,
+      SubmitRecord.Label.Status status, Set<Account.Id> accounts) {
+    String lowerLabel = label.toLowerCase();
+    if (accounts == null || accounts.isEmpty()) {
+      return new SubmitRecordPredicate(status.name() + ',' + lowerLabel);
+    }
+    return Predicate.or(
+        accounts.stream()
+            .map(a -> new SubmitRecordPredicate(
+                status.name() + ',' + lowerLabel + ',' + a.get()))
+            .collect(toList()));
+  }
+
+  private SubmitRecordPredicate(String value) {
+    super(ChangeField.SUBMIT_RECORD, value);
+  }
+
+  @Override
+  public boolean match(ChangeData in) throws OrmException {
+    return ChangeField.formatSubmitRecordValues(in).contains(getValue());
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
new file mode 100644
index 0000000..8782cfd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
@@ -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.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+
+class SubmittablePredicate extends ChangeIndexPredicate {
+  private final SubmitRecord.Status status;
+
+  SubmittablePredicate(SubmitRecord.Status status) {
+    super(ChangeField.SUBMIT_RECORD, status.name());
+    this.status = status;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    return cd.submitRecords(ChangeField.SUBMIT_RULE_OPTIONS_STRICT).stream()
+        .anyMatch(r -> r.status == status);
+  }
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
+}
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 1bc878e..b486bf1 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
@@ -175,6 +175,7 @@
       meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
       grant(config, meta, Permission.READ, admin, owners);
       grant(config, meta, cr, -2, 2, admin, owners);
+      grant(config, meta, Permission.CREATE, admin, owners);
       grant(config, meta, Permission.PUSH, admin, owners);
       grant(config, meta, Permission.SUBMIT, admin, owners);
 
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 a514642..5be1872 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_134> C = Schema_134.class;
+  public static final Class<Schema_135> C = Schema_135.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java
index 9552fa4..a7f57b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_128.java
@@ -47,7 +47,7 @@
   private final PersonIdent serverUser;
 
   @Inject
-  Schema_128(Provider<Schema_126> prior,
+  Schema_128(Provider<Schema_127> prior,
       GitRepositoryManager repoManager,
       AllProjectsName allProjectsName,
       @GerritPersonIdent PersonIdent serverUser) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_135.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_135.java
new file mode 100644
index 0000000..92f150f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_135.java
@@ -0,0 +1,101 @@
+// 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.group.SystemGroupBackend.PROJECT_OWNERS;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+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.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+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.gerrit.server.group.SystemGroupBackend;
+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;
+import java.util.Set;
+import java.util.stream.Stream;
+
+public class Schema_135 extends SchemaVersion {
+  private static final String COMMIT_MSG =
+      "Allow admins and project owners to create refs/meta/config";
+
+  private final GitRepositoryManager repoManager;
+  private final AllProjectsName allProjectsName;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_135(Provider<Schema_134> prior,
+      GitRepositoryManager repoManager,
+      AllProjectsName allProjectsName,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.repoManager = repoManager;
+    this.allProjectsName = allProjectsName;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    try (Repository git = repoManager.openRepository(allProjectsName);
+        MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+            allProjectsName, git)) {
+      ProjectConfig config = ProjectConfig.read(md);
+
+      AccessSection meta = config.getAccessSection(RefNames.REFS_CONFIG, true);
+      Permission createRefsMetaConfigPermission =
+          meta.getPermission(Permission.CREATE, true);
+
+      Set<GroupReference> groups =
+          Stream.concat(
+                  config
+                      .getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
+                      .getPermission(GlobalCapability.ADMINISTRATE_SERVER, true)
+                      .getRules()
+                      .stream()
+                      .map(PermissionRule::getGroup),
+                  Stream.of(SystemGroupBackend.getGroup(PROJECT_OWNERS)))
+              .filter(g -> createRefsMetaConfigPermission.getRule(g) == null)
+              .collect(toSet());
+
+      for (GroupReference group : groups) {
+        createRefsMetaConfigPermission
+            .add(new PermissionRule(config.resolve(group)));
+      }
+
+      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/resources/com/google/gerrit/server/mail/AbandonedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
index 0f16890..bda900b 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AbandonedHtml.soy
@@ -39,6 +39,6 @@
   <hr/>
 
   {if $coverLetter}
-    <pre>{$coverLetter}</pre>
+    <pre style="font-size:12px">{$coverLetter}</pre>
   {/if}
 {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
index 6d04ac3..934ea18 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
@@ -26,6 +26,12 @@
  * @param projectName
  */
 {template .ChangeFooterHtml autoescape="strict" kind="html"}
+  {let $footerStyle kind="css"}
+    color: #555;
+    font-size: 10px;
+    margin: 10px 0 0 0;
+  {/let}
+
   {if $email.changeUrl or $email.settingsUrl}
     <p>
       {if $email.changeUrl}
@@ -38,7 +44,7 @@
     </p>
   {/if}
 
-  <p style="color: #555;">
+  <p style="{$footerStyle}">
     Gerrit-MessageType: {$messageClass}<br/>
     Gerrit-Change-Id: {$changeId}<br/>
     Gerrit-PatchSet: {$patchSet.patchSetId}<br/>
@@ -46,4 +52,13 @@
     Gerrit-Branch: {$branch.shortName}<br/>
     Gerrit-Owner: {$change.ownerEmail}
   </p>
+
+  {if $email.changeUrl}
+    <div itemscope itemtype="http://schema.org/EmailMessage">
+      <div itemscope itemprop="action" itemtype="http://schema.org/ViewAction">
+        <link itemprop="url" href="{$email.changeUrl}"/>
+        <meta itemprop="name" content="View Change"/>
+      </div>
+    </div>
+  {/if}
 {/template}
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
index 781d8a0..ed574a8 100644
--- 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
@@ -23,24 +23,47 @@
  * @param coverLetter
  * @param email
  * @param fromName
+ * @param commentFiles
  */
 {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}
+  {$fromName} has posted comments on this change.
+  {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
   {\n}
-{/template}
\ No newline at end of file
+  Change subject: {$change.subject}{\n}
+  ......................................................................{\n}
+  {if $coverLetter}
+    {\n}
+    {\n}
+    {$coverLetter}{\n}
+    {\n}
+  {/if}
+
+  {foreach $group in $commentFiles}
+    {$group.link}{\n}
+    {$group.title}:{\n}
+    {\n}
+
+    {foreach $comment in $group.comments}
+      {if $comment.isRobotComment}
+        Robot Comment from {$comment.robotId} (run ID {$comment.robotRunId}):
+        {\n}
+      {/if}
+
+      {foreach $line in $comment.lines}
+        {if isFirst($line)}
+          {$comment.linePrefix}
+        {else}
+          {$comment.linePrefixEmpty}
+        {/if}
+        {$line}{\n}
+      {/foreach}
+      {if $comment.parentMessage}
+        >{sp}{$comment.parentMessage}{\n}
+      {/if}
+      {$comment.message}{\n}
+      {\n}
+      {\n}
+    {/foreach}
+  {/foreach}
+  {\n}
+{/template}
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
index 3fcad6b..7ef58b7 100644
--- 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
@@ -20,10 +20,10 @@
  * 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
+ * @param commentFiles
  */
 {template .CommentFooter autoescape="strict" kind="text"}
-  {if $email.hasInlineComments}
+  {if length($commentFiles) > 0}
     Gerrit-HasComments: Yes
   {else}
     Gerrit-HasComments: No
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy
new file mode 100644
index 0000000..f8f6c5f
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooterHtml.soy
@@ -0,0 +1,36 @@
+/**
+ * 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 commentFiles
+ */
+{template .CommentFooterHtml autoescape="strict" kind="html"}
+  {let $footerStyle kind="css"}
+    color: #555;
+    font-size: 10px;
+    margin: 0;
+  {/let}
+
+  <p style="{$footerStyle}">
+    {if length($commentFiles) > 0}
+      Gerrit-HasComments: Yes
+    {else}
+      Gerrit-HasComments: No
+    {/if}
+  </p>
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
new file mode 100644
index 0000000..d55905e
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
@@ -0,0 +1,122 @@
+/**
+ * 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 change
+ * @param commentFiles
+ * @param coverLetter
+ * @param email
+ * @param fromName
+ */
+{template .CommentHtml autoescape="strict" kind="html"}
+  {let $commentHeaderStyle kind="css"}
+    margin-bottom: 4px;
+  {/let}
+
+  {let $blockquoteStyle kind="css"}
+    border-left: 1px solid #aaa;
+    margin: 10px 0;
+    padding: 0 10px;
+  {/let}
+
+  {let $messageStyle kind="css"}
+    white-space: pre-wrap;
+  {/let}
+
+  {let $ulStyle kind="css"}
+    padding-left: 20px;
+  {/let}
+
+  <p>
+    {$fromName} has posted comments on this change.
+  </p>
+
+  {if $email.changeUrl}
+    <p>
+      {call .ViewChangeButton data="all" /}
+    </p>
+  {/if}
+
+  <p>
+    Change subject: {$change.subject}
+  </p>
+  <hr/>
+
+  {if $coverLetter}
+    <pre style="font-size:12px">{$coverLetter}</pre>
+  {/if}
+
+  <ul style="{$ulStyle}">
+    {foreach $group in $commentFiles}
+      <li>
+        <p>
+          <strong><a href="{$group.link}">{$group.title}:</a></strong>
+        </p>
+
+        <ul style="{$ulStyle}">
+          {foreach $comment in $group.comments}
+            <li>
+              {if $comment.isRobotComment}
+                <p style="{$commentHeaderStyle}">
+                  Robot Comment from{sp}
+                  {if $comment.robotUrl}<a href="{$comment.robotUrl}">{/if}
+                  {$comment.robotId}
+                  {if $comment.robotUrl}</a>{/if}{sp}
+                  (run ID {$comment.robotRunId}):
+                </p>
+              {/if}
+
+              <p style="{$commentHeaderStyle}">
+                {if length($comment.lines) > 0}
+                  Patch Set #{$group.patchSetId}, Line {$comment.startLine}:{sp}
+                {/if}
+                {if length($comment.lines) == 1}
+                  <code style="font-size:12px">{$comment.lines[0]}</code>
+                {/if}
+              </p>
+
+              {if length($comment.lines) > 1}
+                <p>
+                  <blockquote style="{$blockquoteStyle}">
+                    <pre style="font-size:12px">
+                      {foreach $line in $comment.lines}
+                        {$line}{\n}
+                      {/foreach}
+                    </pre>
+                  </blockquote>
+                </p>
+              {/if}
+
+              {if $comment.parentMessage}
+                <p>
+                  <blockquote style="{$blockquoteStyle}">
+                    {$comment.parentMessage}
+                  </blockquote>
+                </p>
+              {/if}
+
+              <p style="{$messageStyle}">
+                {$comment.message}
+              </p>
+            </li>
+          {/foreach}
+        </ul>
+      </li>
+    {/foreach}
+  </ul>
+{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
index 2985ef8..e6830ec 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewerHtml.soy
@@ -44,6 +44,6 @@
   <hr/>
 
   {if $coverLetter}
-    <pre>{$coverLetter}</pre>
+    <pre style="font-size:12px">{$coverLetter}</pre>
   {/if}
 {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy
index 8aa430a..6d88748 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVoteHtml.soy
@@ -39,6 +39,6 @@
   <hr/>
 
   {if $coverLetter}
-    <pre>{$coverLetter}</pre>
+    <pre style="font-size:12px">{$coverLetter}</pre>
   {/if}
 {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
index 33dd7b8..083e57d 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -37,16 +37,16 @@
   </p>
   <hr/>
 
-  <pre>
+  <pre style="font-size:12px">
     {$email.changeDetail}
   </pre>
 
-  <pre>
+  <pre style="font-size:12px">
     {$email.approvals}
   </pre>
 
   {if $email.includeDiff}
-    <pre>
+    <pre style="font-size:12px">
       {$email.unifiedDiff}
     </pre>
   {/if}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
index fda960d..95ee41d 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -59,19 +59,19 @@
   </p>
   <hr/>
 
-  <pre>
+  <pre style="font-size:12px">
     {$email.changeDetail}
   </pre>
 
   {if $email.sshHost}
-    <pre>
+    <pre style="font-size:12px">
       git pull ssh:{print '//'}{$email.sshHost}/{$projectName}
           {sp}{$patchSet.refName}
     </pre>
   {/if}
 
   {if $email.includeDiff}
-    <pre>
+    <pre style="font-size:12px">
       {$email.unifiedDiff}
     </pre>
   {/if}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
index 9df9a71..df00e2d 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSetHtml.soy
@@ -59,12 +59,12 @@
   </p>
   <hr/>
 
-  <pre>
+  <pre style="font-size:12px">
     {$email.changeDetail}
   </pre>
 
   {if $email.sshHost}
-    <pre>
+    <pre style="font-size:12px">
       git pull ssh:{print '//'}{$email.sshHost}/{$projectName}{sp}
           {$patchSet.refName}
     </pre>
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy
index 8a7dcc2..83b05c4 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RestoredHtml.soy
@@ -39,6 +39,6 @@
   <hr/>
 
   {if $coverLetter}
-    <pre>{$coverLetter}</pre>
+    <pre style="font-size:12px">{$coverLetter}</pre>
   {/if}
 {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy
index 03d4dea..c0dc4d3 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RevertedHtml.soy
@@ -39,6 +39,6 @@
   <hr/>
 
   {if $coverLetter}
-    <pre>{$coverLetter}</pre>
+    <pre style="font-size:12px">{$coverLetter}</pre>
   {/if}
 {/template}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeFieldTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 839d349..7871437 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -15,10 +15,14 @@
 package com.google.gerrit.server.index.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Table;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.ReviewerSet;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -70,4 +74,63 @@
     assertThat(ChangeField.parseReviewerFieldValues(values))
         .isEqualTo(reviewers);
   }
+
+  @Test
+  public void formatSubmitRecordValues() {
+    assertThat(
+            ChangeField.formatSubmitRecordValues(
+                ImmutableList.of(
+                    record(
+                        SubmitRecord.Status.OK,
+                        label(SubmitRecord.Label.Status.MAY, "Label-1", null),
+                        label(SubmitRecord.Label.Status.OK, "Label-2", 1))),
+                new Account.Id(1)))
+        .containsExactly(
+            "OK",
+            "MAY,label-1",
+            "OK,label-2",
+            "OK,label-2,0",
+            "OK,label-2,1");
+  }
+
+  @Test
+  public void storedSubmitRecords() {
+    assertStoredRecordRoundTrip(record(SubmitRecord.Status.CLOSED));
+    assertStoredRecordRoundTrip(
+        record(
+            SubmitRecord.Status.OK,
+            label(SubmitRecord.Label.Status.MAY, "Label-1", null),
+            label(SubmitRecord.Label.Status.OK, "Label-2", 1)));
+  }
+
+  private static SubmitRecord record(SubmitRecord.Status status,
+      SubmitRecord.Label... labels) {
+    SubmitRecord r = new SubmitRecord();
+    r.status = status;
+    if (labels.length > 0) {
+      r.labels = ImmutableList.copyOf(labels);
+    }
+    return r;
+  }
+
+  private static SubmitRecord.Label label(SubmitRecord.Label.Status status,
+      String label, Integer appliedBy) {
+    SubmitRecord.Label l = new SubmitRecord.Label();
+    l.status = status;
+    l.label = label;
+    if (appliedBy != null) {
+      l.appliedBy = new Account.Id(appliedBy);
+    }
+    return l;
+  }
+
+  private static void assertStoredRecordRoundTrip(SubmitRecord... records) {
+    List<SubmitRecord> recordList = ImmutableList.copyOf(records);
+    List<String> stored = ChangeField.storedSubmitRecords(recordList).stream()
+        .map(s -> new String(s, UTF_8))
+        .collect(toList());
+    assertThat(ChangeField.parseSubmitRecords(stored))
+        .named("JSON %s" + stored)
+        .isEqualTo(recordList);
+  }
 }
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 4758338..1941fd4 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
@@ -1442,6 +1442,41 @@
   }
 
   @Test
+  public void submitRecords() throws Exception {
+    Account.Id user1 = createAccount("user1");
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(repo, newChange(repo));
+    Change change2 = insert(repo, newChange(repo));
+
+    gApi.changes()
+        .id(change1.getId().get())
+        .current()
+        .review(ReviewInput.approve());
+    requestContext.setContext(newRequestContext(user1));
+    gApi.changes()
+        .id(change2.getId().get())
+        .current()
+        .review(ReviewInput.recommend());
+    requestContext.setContext(newRequestContext(user.getAccountId()));
+
+    assertQuery("is:submittable", change1);
+    assertQuery("-is:submittable", change2);
+    assertQuery("submittable:ok", change1);
+    assertQuery("submittable:not_ready", change2);
+
+    assertQuery("label:CodE-RevieW=ok", change1);
+    assertQuery("label:CodE-RevieW=ok,user=user", change1);
+    assertQuery("label:CodE-RevieW=ok,Administrators", change1);
+    assertQuery("label:CodE-RevieW=ok,group=Administrators", change1);
+    assertQuery("label:CodE-RevieW=ok,owner", change1);
+    assertQuery("label:CodE-RevieW=ok,user1");
+    assertQuery("label:CodE-RevieW=need", change2);
+    // NEED records don't have associated users.
+    assertQuery("label:CodE-RevieW=need,user1");
+    assertQuery("label:CodE-RevieW=need,user");
+  }
+
+  @Test
   public void byCommitsOnBranchNotMerged() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     int n = 10;
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index 55c07a6..5c2e9b2 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -19,8 +19,8 @@
 
 maven_jar(
   name = 'compress',
-  id = 'org.apache.commons:commons-compress:1.7',
-  sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d',
+  id = 'org.apache.commons:commons-compress:1.12',
+  sha1 = '84caa68576e345eb5e7ae61a0e5a9229eb100d7b',
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
 )
diff --git a/plugins/replication b/plugins/replication
index b3606eb..bc37211 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit b3606eb38eb8edc166260184177e68386539381a
+Subproject commit bc37211df3cc7b7516974142d78232197c49ce29
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index 3702c84..c910d8f 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -22,6 +22,12 @@
     properties: {
       hasTooltip: Boolean,
 
+      _isTouchDevice: {
+        type: Boolean,
+        value: function() {
+          return 'ontouchstart' in document.documentElement;
+        },
+      },
       _tooltip: Element,
       _titleText: String,
     },
@@ -29,10 +35,10 @@
     attached: function() {
       if (!this.hasTooltip) { return; }
 
-      this.addEventListener('mouseover', this._handleShowTooltip.bind(this));
-      this.addEventListener('mouseout', this._handleHideTooltip.bind(this));
-      this.addEventListener('focusin', this._handleShowTooltip.bind(this));
-      this.addEventListener('focusout', this._handleHideTooltip.bind(this));
+      this.addEventListener('mouseenter', this._handleShowTooltip.bind(this));
+      this.addEventListener('mouseleave', this._handleHideTooltip.bind(this));
+      this.addEventListener('tap', this._handleHideTooltip.bind(this));
+
       this.listen(window, 'scroll', '_handleWindowScroll');
     },
 
@@ -41,6 +47,8 @@
     },
 
     _handleShowTooltip: function(e) {
+      if (this._isTouchDevice) { return; }
+
       if (!this.hasAttribute('title') ||
           this.getAttribute('title') === '' ||
           this._tooltip) {
@@ -66,9 +74,11 @@
     },
 
     _handleHideTooltip: function(e) {
+      if (this._isTouchDevice) { return; }
       if (!this.hasAttribute('title') ||
-          this._titleText == null ||
-          this === document.activeElement) { return; }
+          this._titleText == null) {
+        return;
+      }
 
       this.setAttribute('title', this._titleText);
       if (this._tooltip && this._tooltip.parentNode) {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 48a3d60..8102006 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -42,6 +42,7 @@
       }
       .message {
         flex: 1;
+        max-width: 80ch;
       }
     </style>
     <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
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 5c950dd..007c0fc 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
@@ -120,6 +120,9 @@
         padding-right: 2.6em;
         text-align: right;
       }
+      .warning {
+        color: #666;
+      }
       input.show-hide {
         display: none;
       }
@@ -155,9 +158,18 @@
     <header>
       <div>Files</div>
       <div class="rightControls">
-        <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button>
-        <span class="separator">/</span>
-        <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
+        <template is="dom-if"
+            if="[[_fileListActionsVisible(_numFilesShown, _maxFilesForBulkActions)]]">
+          <gr-button link on-tap="_expandAllDiffs">Show diffs</gr-button>
+          <span class="separator">/</span>
+          <gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
+        </template>
+        <template is="dom-if"
+            if="[[!_fileListActionsVisible(_numFilesShown, _maxFilesForBulkActions)]]">
+          <div class="warning">
+            Bulk file list actions disabled for large amounts of files
+          </div>
+        </template>
         <span class="separator">/</span>
         <select
             id="modeSelect"
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 22c82a0..340cb63 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
@@ -87,6 +87,13 @@
         type: String,
         computed: '_getDiffViewMode(diffViewMode, _userPrefs)',
       },
+      // Caps the number of files that can be shown and have the 'show diffs' /
+      // 'hide diffs' buttons still be functional.
+      _maxFilesForBulkActions: {
+        type: Number,
+        readOnly: true,
+        value: 225,
+      },
     },
 
     behaviors: [
@@ -204,10 +211,8 @@
      */
     _expandAllDiffs: function(e) {
       this._showInlineDiffs = true;
-      for (var i = 0; i < this._files.length; i++) {
-        if (i < this._shownFiles.length) {
-          this.set(['_shownFiles', i, '__expanded'], true);
-        }
+      for (var i = 0; i < this._shownFiles.length; i++) {
+        this.set(['_shownFiles', i, '__expanded'], true);
         this.set(['_files', i, '__expanded'], true);
       }
       if (e && e.target) {
@@ -217,10 +222,8 @@
 
     _collapseAllDiffs: function(e) {
       this._showInlineDiffs = false;
-      for (var i = 0; i < this._files.length; i++) {
-        if (i < this._shownFiles.length) {
-          this.set(['_shownFiles', i, '__expanded'], false);
-        }
+      for (var i = 0; i < this._shownFiles.length; i++) {
+        this.set(['_shownFiles', i, '__expanded'], false);
         this.set(['_files', i, '__expanded'], false);
       }
       this.$.cursor.handleDiffUpdate();
@@ -550,5 +553,9 @@
     _handleDropdownChange: function(e) {
       e.target.blur();
     },
+
+    _fileListActionsVisible: function(numFilesShown, maxFilesForBulkActions) {
+      return numFilesShown <= maxFilesForBulkActions;
+    },
   });
 })();
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 d03bd89..91c6b5e 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
@@ -434,5 +434,28 @@
       assert.equal(select.value, 'SIDE_BY_SIDE');
       document.getElementById('blank').restore();
     });
+
+    test('show/hide diffs disabled for large amounds of files', function(done) {
+      element._files = [];
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: '2',
+      };
+      var computeSpy = sandbox.spy(element, '_fileListActionsVisible');
+      element.selectedIndex = 0;
+      element._numFilesShown = 1;
+      flush(function() {
+        assert.isTrue(computeSpy.lastCall.returnValue);
+        var arr = [];
+        _.times(element._maxFilesForBulkActions + 1, function() {
+          arr.push({__path: 'myfile.txt', __expanded: false});
+        });
+        element._files = arr;
+        element._numFilesShown = arr.length;
+        assert.isFalse(computeSpy.lastCall.returnValue);
+        done();
+      });
+    });
   });
 </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 287e9e4..3649655 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -80,6 +80,9 @@
       .content {
         font-family: var(--monospace-font-family);
       }
+      .message {
+        max-width: 80ch;
+      }
       .collapsed .name,
       .collapsed .content,
       .collapsed .message,
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 875bf2b..fb72dd4 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
@@ -119,24 +119,42 @@
       return expanded ? 'Collapse all' : 'Expand all';
     },
 
+    /**
+     * Computes message author's file comments for change's message.
+     * Method uses this.messages to find next message and relies on messages
+     * to be sorted by date field descending.
+     * @param {!Object} comments Hash of arrays of comments, filename as key.
+     * @param {!Object} message
+     * @return {!Object} Hash of arrays of comments, filename as key.
+     */
     _computeCommentsForMessage: function(comments, message) {
       if (message._index === undefined || !comments || !this.messages) {
         return [];
       }
-      var index = message._index;
       var messages = this.messages || [];
-      var msgComments = {};
-      var mDate = util.parseDate(message.date);
+      var index = message._index;
+      var authorId = message.author._account_id;
+      var mDate = util.parseDate(message.date).getTime();
+      // NB: Messages array has oldest messages first.
       var nextMDate;
-      if (index < messages.length - 1) {
-        nextMDate = util.parseDate(messages[index + 1].date);
+      if (index > 0) {
+        for (var i = index - 1; i >= 0; i--) {
+          if (messages[i].author._account_id === authorId) {
+            nextMDate = util.parseDate(messages[i].date).getTime();
+            break;
+          }
+        }
       }
+      var msgComments = {};
       for (var file in comments) {
         var fileComments = comments[file];
         for (var i = 0; i < fileComments.length; i++) {
-          var cDate = util.parseDate(fileComments[i].updated);
-          if (cDate >= mDate) {
-            if (nextMDate && cDate >= nextMDate) {
+          if (fileComments[i].author._account_id !== authorId) {
+            continue;
+          }
+          var cDate = util.parseDate(fileComments[i].updated).getTime();
+          if (cDate <= mDate) {
+            if (nextMDate && cDate <= nextMDate) {
               continue;
             }
             msgComments[file] = msgComments[file] || [];
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index 28abb0e..01f734b 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -33,6 +33,23 @@
 <script>
   suite('gr-messages-list tests', function() {
     var element;
+    var messages;
+
+    var randomMessage = function(opt_params) {
+      var params = opt_params || {};
+      var author1 = {
+        _account_id: 1115495,
+        name: 'Andrew Bonventre',
+        email: 'andybons@chromium.org',
+      };
+      return {
+        id: params.id || Math.random().toString(),
+        date: params.date || '2016-01-12 20:28:33.038000',
+        message: params.message || Math.random().toString(),
+        _revision_number: params._revision_number || 1,
+        author: params.author || author1,
+      };
+    };
 
     setup(function() {
       stub('gr-rest-api-interface', {
@@ -40,41 +57,8 @@
         getLoggedIn: function() { return Promise.resolve(false); },
       });
       element = fixture('basic');
-      element.messages = [
-        {
-          id: '47c43261_55aa2c41',
-          author: {
-            _account_id: 1115495,
-            name: 'Andrew Bonventre',
-            email: 'andybons@chromium.org',
-          },
-          date: '2016-01-12 20:24:49.448000000',
-          message: 'Uploaded patch set 1.',
-          _revision_number: 1
-        },
-        {
-          id: '47c43261_9593e420',
-          author: {
-            _account_id: 1115495,
-            name: 'Andrew Bonventre',
-            email: 'andybons@chromium.org',
-          },
-          date: '2016-01-12 20:28:33.038000000',
-          message: 'Patch Set 1:\n\n(1 comment)',
-          _revision_number: 1
-        },
-        {
-          id: '87b2aaf4_f73260c5',
-          author: {
-            _account_id: 1143760,
-            name: 'Mark Mentovai',
-            email: 'mark@chromium.org',
-          },
-          date: '2016-01-12 21:17:07.554000000',
-          message: 'Patch Set 1:\n\n(3 comments)',
-          _revision_number: 1
-        }
-      ];
+      messages = _.times(3, randomMessage);
+      element.messages = messages;
       flushAsynchronousOperations();
     });
 
@@ -119,7 +103,7 @@
             'expected gr-message ' + i + ' to not be expanded');
       }
 
-      var messageID = '47c43261_9593e420';
+      var messageID = messages[1].id;
       element.scrollToMessage(messageID);
       assert.isTrue(
           element.$$('[data-message-id="' + messageID + '"]').expanded);
@@ -130,5 +114,99 @@
       scrollToStub.restore();
       highlightStub.restore();
     });
+
+    test('messages', function() {
+      var dates = [
+        '2016-01-12 20:28:33.038000',
+        '2016-01-12 21:28:33.038000',
+        '2016-01-12 22:28:33.038000'
+      ];
+      var author = {
+        _account_id: 42,
+        name: 'Marvin the Paranoid Android',
+        email: 'marvin@sirius.org',
+      };
+      var comments = {
+        file1: [
+          {
+            message: 'message text',
+            updated: '2016-09-27 00:18:03.000000000',
+            in_reply_to: '6505d749_f0bec0aa',
+            line: 62,
+            id: '6505d749_10ed44b2',
+            patch_set: 2,
+            author: {
+              email: 'some@email.com',
+              _account_id: 123,
+            },
+          },
+          {
+            message: 'message text',
+            updated: '2016-09-27 00:18:03.000000000',
+            in_reply_to: 'c5912363_6b820105',
+            line: 42,
+            id: '450a935e_0f1c05db',
+            patch_set: 2,
+            author: author,
+          },
+          {
+            message: 'message text',
+            updated: '2016-09-27 00:18:03.000000000',
+            in_reply_to: '6505d749_f0bec0aa',
+            line: 62,
+            id: '6505d749_10ed44b2',
+            patch_set: 2,
+            author: author,
+          },
+        ],
+        file2: [
+          {
+            message: 'message text',
+            updated: '2016-09-27 00:18:03.000000000',
+            in_reply_to: 'c5912363_4b7d450a',
+            line: 132,
+            id: '450a935e_4f260d25',
+            patch_set: 2,
+            author: author,
+          },
+        ]
+      };
+      var messages = [].concat(
+          randomMessage(),
+          {
+            _index: 5,
+            _revision_number: 4,
+            message: 'Uploaded patch set 4.',
+            date: '2016-09-28 13:36:33.000000000',
+            author: author,
+            id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
+          },
+          {
+            _index: 6,
+            _revision_number: 4,
+            message: 'Patch Set 4:\n\n(6 comments)',
+            date: '2016-09-28 13:36:33.000000000',
+            author: author,
+            id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5',
+          }
+      );
+      element.comments = comments;
+      element.messages = messages;
+      var isAuthor = function(author, message) {
+        return message.author._account_id === author._account_id;
+      };
+      var isMarvin = isAuthor.bind(null, author);
+      flushAsynchronousOperations();
+      var messageElements =
+          Polymer.dom(element.root).querySelectorAll('gr-message');
+      assert.equal(messageElements.length, messages.length);
+      assert.deepEqual(messageElements[1].message, messages[1]);
+      assert.deepEqual(messageElements[2].message, messages[2]);
+      assert.deepEqual(messageElements[1].comments.file1,
+          comments.file1.filter(isMarvin));
+      assert.deepEqual(messageElements[1].comments.file2,
+          comments.file2.filter(isMarvin));
+      assert.deepEqual(messageElements[2].comments, {});
+    });
   });
 </script>
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 f15b554..0e5eb25 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
@@ -235,7 +235,7 @@
       }).then(function() {
         assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay));
         observer = overlayObserver('closed');
-        MockInteractions.tap(yesButton); // confirm the group
+        MockInteractions.tap(yesButton); // Confirm the group.
         return observer;
       }).then(function() {
         assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay));
@@ -430,5 +430,51 @@
         MockInteractions.tap(element.$$('.send'));
       });
     });
+
+    test('do not display tooltips on touch devices', function() {
+      element._account = {_account_id: 1};
+      element.set(['change', 'labels', 'Verified', 'all'],
+          [{_account_id: 1, value: -1}]);
+      element.labels = {
+        Verified: {
+          values: {
+            '-1': 'Fails',
+            ' 0': 'No score',
+            '+1': 'Verified'
+          },
+          default_value: 0
+        },
+        'Code-Review': {
+          values: {
+            '-2': 'Do not submit',
+            '-1': 'I would prefer that you didn\'t submit this',
+            ' 0': 'No score',
+            '+1': 'Looks good to me, but someone else must approve',
+            '+2': 'Looks good to me, approved'
+          },
+          default_value: 0
+        }
+      };
+
+      flushAsynchronousOperations();
+
+      var verifiedBtn = element.$$(
+          'iron-selector[data-label="Verified"] > ' +
+          'gr-button[data-value="-1"]');
+
+      // On touch devices, tooltips should not be shown.
+      verifiedBtn._isTouchDevice = true;
+      verifiedBtn._handleShowTooltip();
+      assert.isNotOk(verifiedBtn._tooltip);
+      verifiedBtn._handleHideTooltip();
+      assert.isNotOk(verifiedBtn._tooltip);
+
+      // On other devices, tooltips should be shown.
+      verifiedBtn._isTouchDevice = false;
+      verifiedBtn._handleShowTooltip();
+      assert.isOk(verifiedBtn._tooltip);
+      verifiedBtn._handleHideTooltip();
+      assert.isNotOk(verifiedBtn._tooltip);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
index 25237b5..d0a49dd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -22,8 +22,7 @@
   <template>
     <style>
       :host {
-        border: 1px solid #ddd;
-        border-right: none;
+        border: 1px solid #bbb;
         display: block;
         white-space: normal;
       }
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 625d6e8..565bfab 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -28,9 +28,10 @@
     <style>
       :host {
         --light-remove-highlight-color: #fee;
-        --dark-remove-highlight-color: #ffd4d4;
+        --dark-remove-highlight-color: rgba(255, 0, 0, 0.15);
         --light-add-highlight-color: #efe;
-        --dark-add-highlight-color: #d4ffd4;
+        --dark-add-highlight-color: rgba(0, 255, 0, 0.15);
+
       }
       :host.no-left .sideBySide ::content .left,
       :host.no-left .sideBySide ::content .left + td,
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 5ee3256..d03ab79 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -86,7 +86,7 @@
         assert.equal(gwtLink.href,
             'http://' + location.host + '/?polygerrit=0#/c/1/1/testfile.txt@2');
         done();
-      })
+      });
     });
 
     test('sets plugins count', function() {
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
index 0b9dc9c..33f6aed 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -119,18 +119,20 @@
     });
 
     test('saves name and preferred email', function(done) {
-      element.$.name.value = 'new name';
-      element.$.email.value = 'email3';
+      flush(function() {
+        element.$.name.value = 'new name';
+        element.$.email.value = 'email3';
 
-      // Nothing should be committed yet.
-      assert.equal(account.name, 'name');
-      assert.equal(account.email, 'email');
+        // Nothing should be committed yet.
+        assert.equal(account.name, 'name');
+        assert.equal(account.email, 'email');
 
-      // Save and verify new values are committed.
-      save().then(function() {
-        assert.equal(account.name, 'new name');
-        assert.equal(account.email, 'email3');
-      }).then(done);
+        // Save and verify new values are committed.
+        save().then(function() {
+          assert.equal(account.name, 'new name');
+          assert.equal(account.email, 'email3');
+        }).then(done);
+      });
     });
 
     test('pressing enter saves name', function(done) {
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
index 8682012..33f7320 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -20,13 +20,11 @@
 
 from __future__ import print_function
 
-import atexit
 import collections
 import json
 import hashlib
 import optparse
 import os
-import shutil
 import subprocess
 import sys
 import tempfile
diff --git a/tools/js/bower2buck.py b/tools/js/bower2buck.py
index 57da475..c9f4789 100755
--- a/tools/js/bower2buck.py
+++ b/tools/js/bower2buck.py
@@ -26,8 +26,7 @@
 import sys
 import tempfile
 
-from tools import util
-
+from tools.js.bowerutil import hash_bower_component
 
 # This script is run with `buck run`, but needs to shell out to buck; this is
 # only possible if we avoid buckd.
@@ -78,7 +77,7 @@
     self.version = bower_json['version']
     self.deps = bower_json.get('dependencies', {})
     self.license = bower_json.get('license', 'NO LICENSE')
-    self.sha1 = bowerutil.hash_bower_component(
+    self.sha1 = hash_bower_component(
         hashlib.sha1(), os.path.dirname(bower_json_path)).hexdigest()
 
   def to_rule(self, packages):
diff --git a/tools/js/bowerutil.py b/tools/js/bowerutil.py
index eb8893b..8e8e835 100644
--- a/tools/js/bowerutil.py
+++ b/tools/js/bowerutil.py
@@ -13,7 +13,6 @@
 # limitations under the License.
 
 import os
-from os import path
 
 
 def hash_bower_component(hash_obj, path):
diff --git a/tools/util.py b/tools/util.py
index 6dd6d59..e8182ed 100644
--- a/tools/util.py
+++ b/tools/util.py
@@ -12,7 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import os
 from os import path
 
 REPO_ROOTS = {