Merge changes If625b6ef,I943b4ab1

* changes:
  Set copyAllScoresIfNoCodeChange for Verified by default
  Set copyAllScoresOnTrivialRebase for Code-Review by default
diff --git a/.buckversion b/.buckversion
index 9c09744..46408a5 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-79d36de9f5284f6e833cca81867d6088a25685fb
+8204fddf60b25a3c2090f3ef0742fca5d466d562
diff --git a/Documentation/.gitignore b/Documentation/.gitignore
new file mode 100644
index 0000000..2d19fc7
--- /dev/null
+++ b/Documentation/.gitignore
@@ -0,0 +1 @@
+*.html
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index ff252f7..acd33c0 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -874,7 +874,7 @@
 If it's desired to have the possibility to upload temporarily hidden
 changes there's a specific permission for that.  This enables someone
 to add specific reviewers for early feedback before making the change
-publically visible.  If you want to allow others than the owners to
+publicly visible.  If you want to allow others than the owners to
 publish a draft you also need to grant them `Publish Drafts`.
 
 Optional access rights to grant:
diff --git a/Documentation/cmd-plugin-ls.txt b/Documentation/cmd-plugin-ls.txt
index 905c9ab..234ce87 100644
--- a/Documentation/cmd-plugin-ls.txt
+++ b/Documentation/cmd-plugin-ls.txt
@@ -14,9 +14,12 @@
 List the installed plugins and show their version and status.
 
 == ACCESS
-* Caller must be a member of the privileged 'Administrators' group.
+* The caller must be a member of a group that is granted the
+  link:access-control.html#capability_viewPlugins[View Plugins]
+  capability or the link:access-control.html#capability_administrateServer[
+  Administrate Server] capability.
 * link:config-gerrit.html#plugins.allowRemoteAdmin[plugins.allowRemoteAdmin]
-must be enabled in `$site_path/etc/gerrit.config`.
+  must be enabled in `$site_path/etc/gerrit.config`.
 
 == SCRIPTING
 This command is intended to be used in scripts.
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
index f31f61b..8fb8e0d 100644
--- a/Documentation/cmd-set-account.txt
+++ b/Documentation/cmd-set-account.txt
@@ -65,7 +65,7 @@
     Delete an email from this user's account if it exists.
     If the email provided is 'ALL', all associated emails are
     deleted from this account.
-    Maybe supplied more than once to remove multiple emails
+    May be supplied more than once to remove multiple emails
     from an account in a single command execution.
 
 --preferred-email::
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 8119d4a..b3e3bc0 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -337,26 +337,27 @@
 [[auth.registerUrl]]auth.registerUrl::
 +
 Target for the "Register" link in the upper right corner.  Used only
-when `auth.type` is `LDAP`.
+when `auth.type` is `LDAP`, `LDAP_BIND` or `CUSTOM_EXTENSION`.
 +
 If not set, no "Register" link is displayed.
 
 [[auth.registerText]]auth.registerText::
 +
 Text for the "Register" link in the upper right corner.  Used only
-when `auth.type` is `LDAP`.
+when `auth.type` is `LDAP`, `LDAP_BIND` or `CUSTOM_EXTENSION`.
 +
 If not set, defaults to "Register".
 
 [[auth.editFullNameUrl]]auth.editFullNameUrl::
 +
 Target for the "Edit" button when the user is allowed to edit their
-full name.
+full name.  Used only when `auth.type` is `LDAP`, `LDAP_BIND` or
+`CUSTOM_EXTENSION`.
 
 [[auth.httpPasswordUrl]]auth.httpPasswordUrl::
 +
 Target for the "Obtain Password" link.  Used only when `auth.type` is
-`LDAP`, `LDAP_BIND` or `CUSTOM_EXTENSION`.
+`CUSTOM_EXTENSION`.
 
 [[auth.switchAccountUrl]]auth.switchAccountUrl::
 +
@@ -1204,7 +1205,7 @@
 Size of the buffer to store logging events for asynchronous logging.
 Putting a larger value can protect threads from stalling when the
 AsyncAppender threads are not fast enough to consume the logging events
-from the buffer. It also protects from loosing log entries in this case.
+from the buffer. It also protects from losing log entries in this case.
 +
 Default is 64 entries.
 
@@ -1451,7 +1452,7 @@
 [[download.archive]]download.archive::
 +
 Specifies which archive formats, if any, should be offered on the change
-screen:
+screen and supported for `git-upload-archive` operation:
 +
 ----
 [download]
@@ -1459,11 +1460,17 @@
   archive = tbz2
   archive = tgz
   archive = txz
+  archive = zip
 ----
 
 If `download.archive` is not specified defaults to all archive
 commands. Set to `off` or empty string to disable.
 
+Zip is not supported because it may be interpreted by a Java plugin as a
+valid JAR file, whose code would have access to cookies on the domain.
+For this reason `zip` format is always excluded from formats offered
+through the `Download` drop down or accessible in the REST API.
+
 [[gc]]
 === Section gc
 
@@ -2189,8 +2196,8 @@
 Number of threads to use for indexing in background operations, such as
 online schema upgrades.
 +
-If not set or set to a negative value, defaults to using the same
-thread pool as interactive operations (unless
+If not set or set to a negative value, defaults to logical number of CPU
+cores as returned by the JVM (unless
 link:#changeMerge.threadPoolSize[changeMerge.threadPoolSize] is set).
 
 [[index.onlineUpgrade]]index.onlineUpgrade::
@@ -2648,6 +2655,22 @@
   javaOptions = -Dcom.sun.jndi.ldap.connect.pool.timeout=300000
 ----
 
+[[log]]
+=== Section log
+
+[[log.jsonLogging]]log.jsonLogging::
++
+If set to true, enables error logging in JSON format (file name: "logs/error_log.json").
++
+Defaults to false.
+
+[[log.textLogging]]log.textLogging::
++
+If set to true, enables error logging in regular plain text format. Can only be disabled
+if `jsonLogging` is enabled.
++
+Defaults to true.
+
 [[mimetype]]
 === Section mimetype
 
@@ -3384,10 +3407,14 @@
 +
 The maximum number of matches evaluated for change access when using full text search.
 +
-Making this number too high could have a negative impact on performance.
-+
 By default 100.
 
+[[suggest.fullTextSearchRefresh]]suggest.fullTextSearchRefresh::
++
+Refresh interval for the in-memory account search index.
++
+By default 1 hour.
+
 
 [[theme]]
 === Section theme
diff --git a/Documentation/config-reverseproxy.txt b/Documentation/config-reverseproxy.txt
index 4ba2b3d..c3dd12e 100644
--- a/Documentation/config-reverseproxy.txt
+++ b/Documentation/config-reverseproxy.txt
@@ -85,7 +85,7 @@
 If you are encountering 'Page Not Found' errors when opening the change
 screen, your Apache proxy is very likely decoding the passed URL.
 Make sure to either use 'AllowEncodedSlashes On' together with
-'ProxyPass .. nodecode' or alternatively a 'mod_rewrite' configuration with
+'ProxyPass .. nocanon' or alternatively a 'mod_rewrite' configuration with
 'AllowEncodedSlashes NoDecode' set.
 
 
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 72d7ddf..b717f4e 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -334,6 +334,8 @@
 plugins that have a dependency on GWT.
 * Update the GWT version in the archetype metadata in the
 `gerrit-plugin-gwt-archetype`.
+* Update the version of `gwt-maven-plugin` in the example pom.xml file in
+link:dev-plugins.html[dev-plugins].
 * Update to the same GWT version in the `gwtjsonrpc` project, and release a
 new version.
 
diff --git a/Documentation/dev-inspector.txt b/Documentation/dev-inspector.txt
index 2d56283..7c13a7d 100644
--- a/Documentation/dev-inspector.txt
+++ b/Documentation/dev-inspector.txt
@@ -240,7 +240,7 @@
 
 ----
 [2012-04-17 14:20:30,558] INFO  com.google.gerrit.pgm.shell.JythonShell : Jython shell instance created.
-[2012-04-17 14:20:38,005] ERROR com.google.gerrit.pgm.shell.JythonShell : Exception occured while loading file Startup.py :
+[2012-04-17 14:20:38,005] ERROR com.google.gerrit.pgm.shell.JythonShell : Exception occurred while loading file Startup.py :
 java.lang.reflect.InvocationTargetException
         at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
         at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 6fc34b1..9bb0007 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -404,6 +404,10 @@
 +
 Publication of usage data
 
+* `com.google.gerrit.extensions.events.GarbageCollectorListener`:
++
+Garbage collection ran on a project
+
 [[stream-events]]
 == Sending Events to the Events Stream
 
@@ -1519,7 +1523,7 @@
 <plugin>
   <groupId>org.codehaus.mojo</groupId>
   <artifactId>gwt-maven-plugin</artifactId>
-  <version>2.5.1</version>
+  <version>2.7.0</version>
   <configuration>
     <module>com.googlesource.gerrit.plugins.myplugin.HelloPlugin</module>
     <disableClassMetadata>true</disableClassMetadata>
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index fc1ef28..1b3e98f 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -157,7 +157,8 @@
 * Build the Gerrit WAR and API JARs
 +
 ----
-  buck build release
+  buck clean
+  buck build --no-cache release
   buck build api_install
 ----
 
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 89661fe..6b83644 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -3,6 +3,7 @@
 == Tutorial
 . Getting started
 .. link:intro-quick.html[A Quick Introduction to Gerrit]
+.. link:intro-user.html[User Guide]
 .. link:intro-project-owner.html[Project Owner Guide]
 .. link:http://source.android.com/submit-patches/workflow[Default Android Workflow] (external)
 . Web
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
new file mode 100644
index 0000000..4584841
--- /dev/null
+++ b/Documentation/intro-user.txt
@@ -0,0 +1,667 @@
+= User Guide
+
+This is a Gerrit guide that is dedicated to Gerrit end-users. It
+explains the standard Gerrit workflows and how a user can adapt Gerrit
+to personal preferences.
+
+It is expected that readers know about link:http://git-scm.com/[Git]
+and that they are familiar with basic git commands and workflows.
+
+[[gerrit]]
+== What is Gerrit?
+
+Gerrit is a Git server that provides link:access-control.html[access
+control] for the hosted Git repositories and a web front-end for doing
+link:#code-review[code review]. Code review is a core functionality of
+Gerrit, but still it is optional and teams can decide to
+link:#no-code-review[work without code review].
+
+[[tools]]
+== Tools
+
+Gerrit speaks the git protocol. This means in order to work with Gerrit
+you do *not* need to install any Gerrit client, but having a regular
+git client, such as the link:http://git-scm.com/[git command line] or
+link:http://eclipse.org/egit/[EGit] in Eclipse, is sufficient.
+
+Still there are some client-side tools for Gerrit, which can be used
+optionally:
+
+* link:http://eclipse.org/mylyn/[Mylyn Gerrit Connector]: Gerrit
+  integration with Mylyn
+* link:https://github.com/uwolfer/gerrit-intellij-plugin[Gerrit
+  IntelliJ Plugin]: Gerrit integration with the
+  link:http://www.jetbrains.com/idea/[IntelliJ Platform]
+* link:https://play.google.com/store/apps/details?id=com.jbirdvegas.mgerrit[
+  mGerrit]: Android client for Gerrit
+* link:https://github.com/stackforge/gertty[Gertty]: Console-based
+  interface for Gerrit
+
+[[clone]]
+== Clone Gerrit Project
+
+Cloning a Gerrit project is done the same way as cloning any other git
+repository by using the `git clone` command.
+
+.Clone Gerrit Project
+----
+  $ git clone ssh://gerrithost:29418/RecipeBook.git RecipeBook
+  Cloning into RecipeBook...
+----
+
+The URL for cloning the project can be found in the Gerrit web UI
+under `Projects` > `List` > <project-name> > `General`.
+
+For git operations Gerrit supports the link:user-upload.html#ssh[SSH]
+and the link:user-upload.html#http[HTTP/HTTPS] protocols.
+
+[NOTE]
+To use SSH you must link:user-upload.html#configure_ssh[generate an SSH
+key pair and upload the public SSH key to Gerrit].
+
+[[code-review]]
+== Code Review Workflow
+
+With Gerrit _Code Review_ means to link:#review-change[review] every
+commit *before* it is accepted into the code base. The author of a code
+modification link:user-upload.html#push_create[uploads a commit] as a
+change to Gerrit. In Gerrit each change is stored in a
+link:#change-ref[staging area] where it can be checked and reviewed.
+Only when it is approved and submitted it gets applied to the code
+base. If there is feedback on a change, the author can improve the code
+modification by link:#upload-patch-set[amending the commit and
+uploading the new commit as a new patch set]. This way a change is
+improved iteratively and it is applied to the code base only when is
+ready.
+
+[[upload-change]]
+== Upload a Change
+
+Uploading a change to Gerrit is done by pushing a commit to Gerrit. The
+commit must be pushed to a ref in the `refs/for/` namespace which
+defines the target branch: `refs/for/<target-branch>`.
+The magic `refs/for/` prefix allows Gerrit to differentiate commits
+that are pushed for review from commits that are pushed directly into
+the repository, bypassing code review. For the target branch it is
+sufficient to specify the short name, e.g. `master`, but you can also
+specify the fully qualified branch name, e.g. `refs/heads/master`.
+
+.Push for Code Review
+----
+  $ git commit
+  $ git push origin HEAD:refs/for/master
+
+  // this is the same as:
+  $ git commit
+  $ git push origin HEAD:refs/for/refs/heads/master
+----
+
+.Push with bypassing Code Review
+----
+  $ git commit
+  $ git push origin HEAD:master
+
+  // this is the same as:
+  $ git commit
+  $ git push origin HEAD:refs/heads/master
+----
+
+[[push-fails]]
+[NOTE]
+If pushing to Gerrit fails consult the Gerrit documentation that
+explains the link:error-messages.html[error messages].
+
+[[change-ref]]
+When a commit is pushed for review, Gerrit stores it in a staging area
+which is a branch in the special `refs/changes/` namespace. A change
+ref has the format `refs/changes/XX/YYYY/ZZ` where `YYYY` is the
+numeric change number, `ZZ` is the patch set number and `XX` is the
+last two digits of the numeric change number, e.g.
+`refs/changes/20/884120/1`. Understanding the format of this ref is not
+required for working with Gerrit.
+
+[[fetch-change]]
+Using the change ref git clients can fetch the corresponding commit,
+e.g. for local verification.
+
+.Fetch Change
+----
+  $ git fetch https://gerrithost/myProject refs/changes/74/67374/2 && git checkout FETCH_HEAD
+----
+
+[NOTE]
+The fetch command can be copied from the
+link:user-review-ui.html#download[download commands] in the change
+screen.
+
+The `refs/for/` prefix is used to map the Gerrit concept of
+"Pushing for Review" to the git protocol. For the git client it looks
+like every push goes to the same branch, e.g. `refs/for/master` but in
+fact for each commit that is pushed to this ref Gerrit creates a new
+branch under the `refs/changes/` namespace. In addition Gerrit creates
+an open change.
+
+[[change]]
+A change consists of a link:user-changeid.html[Change-Id], meta data
+(owner, project, target branch etc.), one or more patch sets, comments
+and votes. A patch set is a git commit. Each patch set in a change
+represents a new version of the change and replaces the previous patch
+set. Only the latest patch set is relevant. This means all failed
+iterations of a change will never be applied to the target branch, but
+only the last patch set that is approved is integrated.
+
+[[change-id]]
+The Change-Id is important for Gerrit to know whether a commit that is
+pushed for code review should create a new change or whether it should
+create a new patch set for an existing change.
+
+The Change-Id is a SHA-1 that is prefixed with an uppercase `I`. It is
+specified as footer in the commit message (last paragraph):
+
+----
+  Improve foo widget by attaching a bar.
+
+  We want a bar, because it improves the foo by providing more
+  wizbangery to the dowhatimeanery.
+
+  Bug: #42
+  Change-Id: Ic8aaa0728a43936cd4c6e1ed590e01ba8f0fbf5b
+  Signed-off-by: A. U. Thor <author@example.com>
+----
+
+If a commit that has a Change-Id in its commit message is pushed for
+review, Gerrit checks if a change with this Change-Id already exists
+for this project and target branch, and if yes, Gerrit creates a new
+patch set for this change. If not, a new change with the given
+Change-Id is created.
+
+If a commit without Change-Id is pushed for review, Gerrit creates a
+new change and generates a Change-Id for it. Since in this case the
+Change-Id is not included in the commit message, it must be manually
+inserted when a new patch set should be uploaded. Most projects already
+link:project-configuration.html#require-change-id[require a Change-Id]
+when pushing the very first patch set. This reduces the risk of
+accidentally creating a new change instead of uploading a new patch
+set. Any push without Change-Id then fails with
+link:error-missing-changeid.html[missing Change-Id in commit message
+footer]. New patch sets can always be uploaded to a specific change
+(even without any Change-Id) by pushing to the change ref, e.g.
+`refs/changes/74/67374`.
+
+Amending and rebasing a commit preserves the Change-Id so that the new
+commit automatically becomes a new patch set of the existing change,
+when it is pushed for review.
+
+.Push new Patch Set
+----
+  $ git commit --amend
+  $ git push origin HEAD:refs/for/master
+----
+
+Change-Ids are unique for a branch of a project. E.g. commits that fix
+the same issue in different branches should have the same Change-Id,
+which happens automatically if a commit is cherry-picked to another
+branch. This way you can link:user-search.html[search] by the Change-Id
+in the Gerrit web UI to find a fix in all branches.
+
+Change-Ids can be created automatically by installing the `commit-msg`
+hook as described in the link:user-changeid.html#creation[Change-Id
+documentation].
+
+Instead of manually installing the `commit-msg` hook for each git
+repository, you can copy it into the
+link:http://git-scm.com/docs/git-init#_template_directory[git template
+directory]. Then it is automatically copied to every newly cloned
+repository.
+
+[[review-change]]
+== Review Change
+
+After link:#upload-change[uploading a change for review] reviewers can
+inspect it via the Gerrit web UI. Reviewers can see the code delta and
+link:user-review-ui.html#inline-comments[comment directly in the code]
+on code blocks or lines. They can also link:user-review-ui.html#reply[
+post summary comments and vote on review labels]. The
+link:user-review-ui.html[documentation of the review UI] explains the
+screens and controls for doing code reviews.
+
+There are several options to control how patch diffs should be
+rendered. Users can configure their preferences in the
+link:user-review-ui.html#diff-preferences[diff preferences].
+
+[[upload-patch-set]]
+== Upload a new Patch Set
+
+If there is feedback from code review and a change should be improved a
+new patch set with the reworked code should be uploaded.
+
+This is done by amending the commit of the last patch set. If needed
+this commit can be fetched from Gerrit by using the fetch command from
+the link:user-review-ui.html#download[download commands] in the change
+screen.
+
+It is important that the commit message contains the
+link:user-changeid.html[Change-Id] of the change that should be updated
+as a footer (last paragraph). Normally the commit message already
+contains the correct Change-Id and the Change-Id is preserved when the
+commit is amended.
+
+.Push Patch Set
+----
+  // fetch and checkout the change
+  // (checkout command copied from change screen)
+  $ git fetch https://gerrithost/myProject refs/changes/74/67374/2 && git checkout FETCH_HEAD
+
+  // rework the change
+  $ git add <path-of-reworked-file>
+  ...
+
+  // amend commit
+  $ git commit --amend
+
+  // push patch set
+  $ git push origin HEAD:refs/for/master
+----
+
+[NOTE]
+Never amend a commit that is already part of a central branch.
+
+Pushing a new patch set triggers email notification to the reviewers.
+
+[[multiple-features]]
+== Developing multiple Features in parallel
+
+Code review takes time, which can be used by the change author to
+implement other features. Each feature should be implemented in its own
+local feature branch that is based on the current HEAD of the target
+branch. This way there is no dependency to open changes and new
+features can be reviewed and applied independently. If wanted, it is
+also possible to base a new feature on an open change. This will create
+a dependency between the changes in Gerrit and each change can only be
+applied if all its predecessor are applied as well. Dependencies
+between changes can be seen from the
+link:user-review-ui.html#related-changes-tab[Related Changes] tab on
+the change screen.
+
+[[watch]]
+== Watching Projects
+
+To get to know about new changes you can link:user-notify.html#user[
+watch the projects] that you are interested in. For watched projects
+Gerrit sends you email notifications when a change is uploaded or
+modified. You can decide on which events you want to be notified and
+you can filter the notifications by using link:user-search.html[change
+search expressions]. For example '+branch:master file:^.*\.txt$+' would
+send you email notifications only for changes in the master branch that
+touch a 'txt' file.
+
+It is common that the members of a project team watch their own
+projects and then pick the changes that are interesting to them for
+review.
+
+Project owners may also configure
+link:intro-project-owner.html#notifications[notifications on
+project-level].
+
+[[adding-reviewers]]
+== Adding Reviewers
+
+In the link:user-review-ui.html#reviewers[change screen] reviewers can
+be added explicitly to a change. The added reviewer will then be
+notified by email about the review request.
+
+Mainly this functionality is used to request the review of specific
+person who is known to be an expert in the modified code or who is a
+stakeholder of the implemented feature. Normally it is not needed to
+explicitly add reviewers on every change, but you rather rely on the
+project team to watch their project and to process the incoming changes
+by importance, interest, time etc.
+
+There are also link:intro-project-owner.html#reviewers[plugins which
+can add reviewers automatically] (e.g. by configuration or based on git
+blame annotations). If this functionality is required it should be
+discussed with the project owners and the Gerrit administrators.
+
+[[dashboards]]
+== Dashboards
+
+Gerrit supports a wide range of link:user-search.html#search-operators[
+query operators] to search for changes by different criteria, e.g. by
+status, change owner, votes etc.
+
+The page that shows the results of a change query has the change query
+contained in its URL. This means you can bookmark this URL in your
+browser to save the change query. This way it can be easily re-executed
+later.
+
+Several change queries can be also combined into a dashboard. A
+dashboard is a screen in Gerrit that presents the results of several
+change queries in different sections, each section having a descriptive
+title.
+
+A default dashboard is available under `My` > `Changes`. It has
+sections to list outgoing reviews, incoming reviews and recently closed
+changes.
+
+Users can also define link:user-dashboards.html#custom-dashboards[
+custom dashboards]. Dashboards can be bookmarked in a browser so that
+they can be re-executed later.
+
+It is also possible to link:#my-menu[customize the My menu] and add
+menu entries for custom queries or dashboards to it.
+
+Dashboards are very useful to define own views on changes, e.g. you can
+have different dashboards for own contributions, for doing reviews or
+for different sets of projects.
+
+[NOTE]
+You can use the link:user-search.html#limit[limit] and
+link:user-search.html#age[age] query operators to limit the result set
+in a dashboard section. Clicking on the section title executes the
+change query without the `limit` and `age` operator so that you can
+inspect the full result set.
+
+Project owners can also define shared
+link:user-dashboards.html#project-dashboards[dashboards on
+project-level]. The project dashboards can be seen in the web UI under
+`Projects` > `List` > <project-name> > `Dashboards`.
+
+[[submit]]
+== Submit a Change
+
+Submitting a change means that the code modifications of the current
+patch set are applied to the target branch. Submit requires the
+link:access-control.html#category_submit[Submit] access right and is
+done on the change screen by clicking on the
+link:user-review-ui.html#submit[Submit] button.
+
+In order to be submittable changes must first be approved by
+link:user-review-ui.html#vote[voting on the review labels]. By default
+a change can only be submitted if it has a vote with the highest value
+on each review label and no vote with the lowest value (veto vote).
+Projects can configure link:intro-project-owner.html#labels[custom
+labels] and link:intro-project-owner.html#submit-rules[custom submit
+rules] to control when a change becomes submittable.
+
+How the code modification is applied to the target branch when a change
+is submitted is controlled by the
+link:project-configuration.html#submit_type[submit type] which can be
+link:intro-project-owner.html#submit-type[configured on project-level].
+
+Submitting a change may fail with conflicts. In this case you need to
+link:#rebase[rebase] the change locally, resolve the conflicts and
+upload the commit with the conflict resolution as new patch set.
+
+If a change cannot be merged due to path conflicts this is highlighted
+on the change screen by a bold red `Cannot Merge` label.
+
+[[rebase]]
+== Rebase a Change
+
+While a change is in review the HEAD of the target branch can evolve.
+In this case the change can be rebased onto the new HEAD of the target
+branch. When there are no conflicts the rebase can be done directly
+from the link:user-review-ui.html#rebase[change screen], otherwise it
+must be done locally.
+
+.Rebase a Change locally
+----
+  // update the remote tracking branches
+  $ git fetch
+
+  // fetch and checkout the change
+  // (checkout command copied from change screen)
+  $ git fetch https://gerrithost/myProject refs/changes/74/67374/2 && git checkout FETCH_HEAD
+
+  // do the rebase
+  $ git rebase origin/master
+
+  // resolve conflicts if needed and stage the conflict resolution
+  ...
+  $ git add <path-of-file-with-conflicts-resolved>
+
+  // continue the rebase
+  $ git rebase --continue
+
+  // push the commit with the conflict resolution as new patch set
+  $ git push origin HEAD:refs/for/master
+----
+
+Doing a manual rebase is only necessary when there are conflicts that
+cannot be resolved by Gerrit. If manual conflict resolution is needed
+also depends on the link:intro-project-owner.html#submit-type[submit
+type] that is configured for the project.
+
+Generally changes shouldn't be rebased without reason as it
+increases the number of patch sets and creates noise with
+notifications. However if a change is in review for a long time it may
+make sense to rebase it from time to time, so that reviewers can see
+the delta against the current HEAD of the target branch. It also shows
+that there is still an interest in this change.
+
+[NOTE]
+Never rebase commits that are already part of a central branch.
+
+[[abandon]]
+[[restore]]
+== Abandon/Restore a Change
+
+Sometimes during code review a change is found to be bad and it should
+be given up. In this case the change can be
+link:user-review-ui.html#abandon[abandoned] so that it doesn't appear
+in list of open changes anymore.
+
+Abandoned changes can be link:user-review-ui.html#restore[restored] if
+later they are needed again.
+
+[[topics]]
+== Using Topics
+
+Changes can be grouped by topics. This is useful because it allows you
+to easily find related changes by using the
+link:user-search.html#topic[topic search operator]. Also on the change
+screen link:user-review-ui.html#same-topic[changes with the same topic]
+are displayed so that you can easily navigate between them.
+
+Often changes that together implement a feature or a user story are
+group by a topic.
+
+Assigning a topic to a change can be done in the
+link:user-review-ui.html#project-branch-topic[change screen].
+
+It is also possible to link:user-upload.html#topic[set a topic on
+push].
+
+.Set Topic on Push
+----
+  $ git push origin HEAD:refs/for/master%topic=multi-master
+----
+
+[[drafts]]
+== Working with Drafts
+
+Changes can be uploaded as drafts. By default draft changes are only
+visible to the change owner. This gives you the possibility to have
+some staging before making your changes visible to the reviewers. Draft
+changes can also be used to backup unfinished changes.
+
+A draft change is created by pushing to the magic
+`refs/drafts/<target-branch>` ref.
+
+.Push a Draft Change
+----
+  $ git commit
+  $ git push origin HEAD:refs/drafts/master
+----
+
+Draft changes have the state link:user-review-ui.html#draft[Draft] and
+can be link:user-review-ui.html#publish[published] or
+link:user-review-ui.html#delete[deleted] from the change screen.
+
+By link:user-review-ui.html#reviewers[adding reviewers] to a draft
+change the change is made visible to these users. This way you can
+collaborate with other users in privacy.
+
+By pushing to `refs/drafts/<target-branch>` you can also upload draft
+patch sets to non-draft changes. Draft patch sets are immediately
+visible to all reviewers of the change, but other users cannot see the
+draft patch set. A draft patch set can be published and deleted in the
+same way as a draft change.
+
+[[inline-edit]]
+== Inline Edit
+
+It is possible to link:user-inline-edit.html#editing-change[edit
+changes inline] directly in the web UI. This is useful to make small
+corrections immediately and publish them as a new patch set.
+
+It is also possible to link:user-inline-edit.html#create-change[create
+new changes inline].
+
+[[project-administration]]
+== Project Administration
+
+Every project has a link:intro-project-owner.html#project-owner[project
+owner] that administrates the project. Project administration includes
+the configuration of the project
+link:intro-project-owner.html#access-rights[access rights], but project
+owners have many more possibilities to customize the workflows for a
+project which are described in the link:intro-project-owner.html[
+project owner guide].
+
+[[no-code-review]]
+== Working without Code Review
+
+Doing code reviews with Gerrit is optional and you can use Gerrit
+without code review as a pure Git server.
+
+.Push with bypassing Code Review
+----
+  $ git commit
+  $ git push origin HEAD:master
+
+  // this is the same as:
+  $ git commit
+  $ git push origin HEAD:refs/heads/master
+----
+
+[NOTE]
+Bypassing code review must be enabled in the project access rights. The
+project owner must allow it by assigning the
+link:access-control.html#category_push_direct[Push] access right on the
+target branch (`refs/heads/<branch-name>`).
+
+[NOTE]
+If you bypass code review you always need to merge/rebase manually if
+the tip of the destination branch has moved. Please keep this in mind
+if you choose to not work with code review because you think it's
+easier to avoid the additional complexity of the review workflow; it
+might actually not be easier.
+
+[NOTE]
+The project owner may enable link:user-upload.html#auto_merge[
+auto-merge on push] to benefit from the automatic merge/rebase on
+server side while pushing directly into the repository.
+
+[[preferences]]
+== Preferences
+
+There are several options to control the rendering in the Gerrit web UI.
+Users can configure their preferences under `Settings` > `Preferences`.
+
+The following preferences can be configured:
+
+- [[show-site-header]]`Show Site Header`:
++
+Whether the site header should be shown.
+
+- [[use-flash]]`Use Flash Clipboard Widget`:
++
+Whether the Flash clipboard widget should be used. If enabled Gerrit
+offers a copy-to-clipboard icon next to IDs and commands that need to
+be copied frequently, such as the Change-Ids, commit IDs and download
+commands.
+
+- [[cc-me]]`CC Me On Comments I Write`:
++
+Whether you get notified by email as CC on comments that you write
+yourself.
+
+- [[review-category]]`Display In Review Category`:
++
+This setting controls how the values of the review labels in change
+lists and dashboards are visualized.
++
+** `None`:
++
+For each review label only the voting value is shown. Approvals are
+rendered as a green check mark icon, vetos as a red X icon.
++
+** `Show Name`:
++
+For each review label the voting value is shown together with the full
+name of the voting user.
++
+** `Show Email`:
++
+For each review label the voting value is shown together with the email
+address of the voting user.
++
+** `Show Username`:
++
+For each review label the voting value is shown together with the
+username of the voting user.
++
+** `Show Abbreviated Name`:
++
+For each review label the voting value is shown together with the
+initials of the full name of the voting user.
+
+- [[page-size]]`Maximum Page Size`:
++
+The maximum number of entries that are shown on one page, e.g. used
+when paging through changes, projects, branches or groups.
+
+- [[date-time-format]]`Date/Time Format`:
++
+The format that should be used to render dates and timestamps.
+
+- [[relative-dates]]`Show Relative Dates In Changes Table`:
++
+Whether timestamps in change lists and dashboards should be shown as
+relative timestamps, e.g. '12 days ago' instead of absolute timestamps
+such as 'Apr 15'.
+
+- [[change-size-bars]]`Show Change Sizes As Colored Bars`:
++
+Whether change sizes should be visualized as colored bars. If disabled
+the numbers of added and deleted lines are shown as text, e.g.
+'+297, -63'.
+
+- [[show-change-number]]`Show Change Number In Changes Table`:
++
+Whether in change lists and dashboards an `ID` column with the numeric
+change IDs should be shown.
+
+- [[mute-common-path-prefixes]]`Mute Common Path Prefixes In File List`:
++
+Whether common path prefixes in the file list on the change screen
+should be link:user-review-ui.html#repeating-path-segments[grayed out].
+
+- [[diff-view]]`Diff View`:
++
+Whether the Side-by-Side diff view or the Unified diff view should be
+shown when clicking on a file path in the change screen.
+
+[[my-menu]]
+In addition it is possible to customize the menu entries of the `My`
+menu. This can be used to make the navigation to frequently used
+screens, e.g. configured link:#dashboards[dashboards], quick.
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/json.txt b/Documentation/json.txt
index feef1a1..8ccd03b 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -156,7 +156,7 @@
 
 newRev:: The new value the ref was updated to.
 
-refName:: Ref name within project.
+refName:: Full ref name within project.
 
 project:: Project path in Gerrit.
 
diff --git a/Documentation/project-configuration.txt b/Documentation/project-configuration.txt
index e8f870f..6d7c6d0 100644
--- a/Documentation/project-configuration.txt
+++ b/Documentation/project-configuration.txt
@@ -169,6 +169,7 @@
 where a commit is already merged into a branch and you want to create
 a new open change for that commit on another branch.
 
+[[require-change-id]]
 === Require Change-Id
 
 The `Require Change-Id in commit message` option defines whether a
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 43caa83..b15c283 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -141,15 +141,14 @@
 
 * `ok(user(ID))` or just `ok(_)` if user info is not important. This status is
   used to tell that this label/category has been met.
-* `need(_)` is used to tell that this label/category is needed for change to
-  become submittable
-* `reject(user(ID))` or just `reject(_)`. This status is used to tell that
-  label/category is blocking change submission
-* `impossible(_)` is used when the logic knows that the change cannot be
-  submitted as-is. Administrative intervention is probably required. This is
-  meant for cases where the logic requires members of "FooEng" to score
-  `Code-Review +2` on a change, but nobody is in group "FooEng". It is to hint
-  at permissions misconfigurations.
+* `need(_)` is used to tell that this label/category is needed for the change to
+   become submittable.
+* `reject(user(ID))` or just `reject(_)`. This status is used to tell that this
+   label/category is blocking submission of the change.
+* `impossible(_)` is used when the logic knows that the change cannot be submitted
+   as-is. This is meant for cases where the logic requires members of a specific
+   group to apply a specific label on a change, but no users are in that group.
+   This is usually caused by misconfiguration of permissions.
 * `may(_)` allows expression of approval categories that are optional, i.e.
   could either be set or unset without ever influencing whether the change
   could be submitted.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 8c10bc7..138f85f 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -310,6 +310,13 @@
 * `CHECK`: include potential problems with the change.
 --
 
+[[commit-footers]]
+--
+* `COMMIT_FOOTERS`: include the full commit message with
+  Gerrit-specific commit footers in the
+  link:#revision-info[RevisionInfo].
+--
+
 .Request
 ----
   GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES&o=DOWNLOAD_COMMANDS HTTP/1.0
@@ -1146,6 +1153,109 @@
   HTTP/1.1 204 No Content
 ----
 
+[[list-change-comments]]
+=== List Change Comments
+--
+'GET /changes/link:#change-id[\{change-id\}]/comments'
+--
+
+Lists the published comments of all revisions of the change.
+
+Returns a map of file paths to lists of link:#comment-info[CommentInfo]
+entries. The entries in the map are sorted by file path, and the
+comments for each path are sorted by patch set number. Each comment has
+the `patch_set` and `author` fields set.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/comments HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+      {
+        "patch_set": 1,
+        "id": "TvcXrmjM",
+        "line": 23,
+        "message": "[nit] trailing whitespace",
+        "updated": "2013-02-26 15:40:43.986000000"
+        "author": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com"
+        }
+      },
+      {
+        "patch_set": 2,
+        "id": "TveXwFiA",
+        "line": 49,
+        "in_reply_to": "TfYX-Iuo",
+        "message": "Done",
+        "updated": "2013-02-26 15:40:45.328000000"
+        "author": {
+          "_account_id": 1000097,
+          "name": "Jane Roe",
+          "email": "jane.roe@example.com"
+        }
+      }
+    ]
+  }
+----
+
+[[list-change-drafts]]
+=== List Change Drafts
+--
+'GET /changes/link:#change-id[\{change-id\}]/drafts'
+--
+
+Lists the draft comments of all revisions of the change that belong to
+the calling user.
+
+Returns a map of file paths to lists of link:#comment-info[CommentInfo]
+entries. The entries in the map are sorted by file path, and the
+comments for each path are sorted by patch set number. Each comment has
+the `patch_set` field set, and no `author`.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/drafts HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+      {
+        "patch_set": 1,
+        "id": "TvcXrmjM",
+        "line": 23,
+        "message": "[nit] trailing whitespace",
+        "updated": "2013-02-26 15:40:43.986000000"
+      },
+      {
+        "patch_set": 2,
+        "id": "TveXwFiA",
+        "line": 49,
+        "in_reply_to": "TfYX-Iuo",
+        "message": "Done",
+        "updated": "2013-02-26 15:40:45.328000000"
+      }
+    ]
+  }
+----
+
 [[check-change]]
 === Check change
 --
@@ -1577,7 +1687,7 @@
 ----
 
 When change was rebased on top of latest patch set, response
-"`204 No Content`" is returned. When change edit is aready
+"`204 No Content`" is returned. When change edit is already
 based on top of the latest patch set, the response
 "`409 Conflict`" is returned.
 
@@ -2586,7 +2696,7 @@
 ----
 
 [[list-drafts]]
-=== List Drafts
+=== List Revision Drafts
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/drafts/'
 --
@@ -2594,9 +2704,8 @@
 Lists the draft comments of a revision that belong to the calling
 user.
 
-As result a map is returned that maps the file path to a list of
-link:#comment-info[CommentInfo] entries. The entries in the map are
-sorted by file path.
+Returns a map of file paths to lists of link:#comment-info[CommentInfo]
+entries. The entries in the map are sorted by file path.
 
 .Request
 ----
@@ -2765,7 +2874,7 @@
 ----
 
 [[list-comments]]
-=== List Comments
+=== List Revision Comments
 --
 'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/'
 --
@@ -3494,6 +3603,9 @@
 [options="header",cols="1,^1,5"]
 |===========================
 |Field Name    ||Description
+|`patch_set`   |optional|
+The patch set number for the comment; only set in contexts where +
+comments may be returned for multiple patch sets.
 |`id`          ||The URL encoded UUID of the comment.
 |`path`        |optional|
 The path of the file for which the inline comment was done. +
@@ -4013,7 +4125,9 @@
 Draft handling that defines how draft comments are handled that are
 already in the database but that were not also described in this
 input. +
-Allowed values are `DELETE`, `PUBLISH` and `KEEP`. +
+Allowed values are `DELETE`, `PUBLISH`, `PUBLISH_ALL_REVISIONS` and
+`KEEP`. All values except `PUBLISH_ALL_REVISIONS` operate only on drafts
+for a single revision. +
 If not set, the default is `DELETE`.
 |`notify`      |optional|
 Notify handling that defines to whom email notifications should be sent
@@ -4108,6 +4222,12 @@
 |`reviewed`     |optional|
 Indicates whether the caller is authenticated and has commented on the
 current revision. Only set if link:#reviewed[REVIEWED] option is requested.
+|`messageWithFooter` |optional|
+If the link:#commit-footers[COMMIT_FOOTERS] option is requested and
+this is the current patch set, contains the full commit message with
+Gerrit-specific commit footers, as if this revision were submitted
+using the link:project-configuration.html#cherry_pick[Cherry Pick]
+submit type.
 |===========================
 
 [[rule-input]]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 0ee6966..a8bbbfa 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -30,6 +30,84 @@
   "2.7"
 ----
 
+[[get-info]]
+=== Get Server Info
+--
+'GET /config/server/info'
+--
+
+Returns the information about the Gerrit server configuration.
+
+.Request
+----
+  GET /config/server/info HTTP/1.0
+----
+
+As result a link:#server-info[ServerInfo] entity is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "auth": {
+      "auth_type": "LDAP",
+      "editable_account_fields": [
+        "FULL_NAME",
+        "REGISTER_NEW_EMAIL"
+      ]
+    },
+    "download": {
+      "schemes": [
+        "anonymous http": {
+          "url": "http://gerrithost:8080/${project}",
+          "commands": {
+            "Checkout": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git checkout FETCH_HEAD",
+            "Format Patch": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
+            "Pull": "git pull http://gerrithost:8080/${project} ${ref}",
+            "Cherry Pick": "git fetch http://gerrithost:8080/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
+          }
+        },
+        "http": {
+          "url": "http://jdoe@gerrithost:8080/${project}",
+          "is_auth_required": true,
+          "is_auth_supported": true,
+          "commands": {
+            "Checkout": "git fetch http://jdoe@gerrithost:8080/${project} ${ref} \u0026\u0026 git checkout FETCH_HEAD",
+            "Format Patch": "git fetch http://jdoe@gerrithost:8080/${project} ${ref} \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
+            "Pull": "git pull http://jdoe@gerrithost:8080/${project} ${ref}",
+            "Cherry Pick": "git fetch http://jdoe@gerrithost:8080/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
+          }
+        },
+        "ssh": {
+          "url": "ssh://jdoe@gerrithost:29418/${project}",
+          "is_auth_required": true,
+          "is_auth_supported": true,
+          "commands": {
+            "Checkout": "git fetch ssh://jdoe@gerrithost:29418/${project} ${ref} \u0026\u0026 git checkout FETCH_HEAD",
+            "Format Patch": "git fetch ssh://jdoe@gerrithost:29418/${project} ${ref} \u0026\u0026 git format-patch -1 --stdout FETCH_HEAD",
+            "Pull": "git pull ssh://jdoe@gerrithost:29418/${project} ${ref}",
+            "Cherry Pick": "git fetch ssh://jdoe@gerrithost:29418/${project} ${ref} \u0026\u0026 git cherry-pick FETCH_HEAD"
+          }
+        }
+      ],
+      "archives": [
+        "tgz",
+        "tar",
+        "tbz2",
+        "txz"
+      ]
+    },
+    "gerrit": {
+      "all_projects": "All-Projects",
+      "all_users": "All-Users"
+    }
+  }
+----
+
+
 [[list-caches]]
 === List Caches
 --
@@ -822,6 +900,27 @@
 [[json-entities]]
 == JSON Entities
 
+[[auth-info]]
+=== AuthInfo
+The `AuthInfo` entity contains information about the authentication
+configuration of the Gerrit server.
+
+[options="header",cols="1,^1,5"]
+|==========================================
+|Field Name                   ||Description
+|`type`                       ||
+The link:config-gerrit.html#auth.type[authentication type] that is
+configured on the server. Can be `OPENID`, `OPENID_SSO`, `OAUTH`,
+`HTTP`, `HTTP_LDAP`, `CLIENT_SSL_CERT_LDAP`, `LDAP`, `LDAP_BIND`,
+`CUSTOM_EXTENSION` or `DEVELOPMENT_BECOME_ANY_ACCOUNT`.
+|`use_contributor_agreements` |not set if `false`|
+Whether link:config-gerrit.html#auth.contributorAgreements[contributor
+agreements] are required.
+|`editable_account_fields`    ||
+List of account fields that are editable. Possible values are
+`FULL_NAME`, `USER_NAME` and `REGISTER_NEW_EMAIL`.
+|==========================================
+
 [[cache-info]]
 === CacheInfo
 The `CacheInfo` entity contains information about a cache.
@@ -878,6 +977,59 @@
 |`name`               |capability name
 |=================================
 
+[[contact-store-info]]
+=== ContactStoreInfo
+The `ContactStoreInfo` entity contains information about the contact
+store.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name |Description
+|`url`      |
+The link:config-gerrit.html#contactstore.url[URL of the contact store].
+|=======================
+
+[[download-info]]
+=== DownloadInfo
+The `DownloadInfo` entity contains information about supported download
+options.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name |Description
+|`schemes`  |
+The supported download schemes as a map which maps the scheme name to a
+of link:#download-scheme-info[DownloadSchemeInfo] entity.
+|`archives` |
+List of supported archive formats. Possible values are `tgz`, `tar`,
+`tbz2` and `txz`.
+|=======================
+
+[[download-scheme-info]]
+=== DownloadSchemeInfo
+The `DownloadSchemeInfo` entity contains information about a supported
+download scheme and its commands.
+
+[options="header",cols="1,^1,5"]
+|=================================
+|Field Name          ||Description
+|`url`               ||
+The URL of the download scheme, where '${project}' is used as
+placeholder for the project name.
+|`is_auth_required`  |not set if `false`|
+Whether this download scheme requires authentication.
+|`is_auth_supported` |not set if `false`|
+Whether this download scheme supports authentication.
+|`commands`          ||
+Download commands as a map which maps the command name to the download
+command. In the download command '${project}' is used as
+placeholder for the project name, and '${ref}' is used as
+placeholder for the (change) ref.
+
+Empty, if accessed anonymously and the download scheme requires
+authentication.
+|=================================
+
 [[entries-info]]
 === EntriesInfo
 The `EntriesInfo` entity contains information about the entries in a
@@ -896,6 +1048,21 @@
 `g`: gigabytes). Only set for disk caches.
 |==================================
 
+[[gerrit-info]]
+=== GerritInfo
+The `GerritInfo` entity contains information about Gerrit
+configuration from the link:config-gerrit.html#gerrit[gerrit] section.
+
+[options="header",cols="1,6"]
+|================================
+|Field Name          |Description
+|`all_projects_name` |
+Name of the link:config-gerrit.html#gerrit.allProjects[root project].
+|`all_users_name`    |
+Name of the link:config-gerrit.html#gerrit.allUsers[project in which
+meta data of all users is stored].
+|================================
+
 [[hit-ration-info]]
 === HitRatioInfo
 The `HitRatioInfo` entity contains information about the hit ratio of a
@@ -958,6 +1125,30 @@
 The number of open files.
 |============================
 
+[[server-info]]
+=== ServerInfo
+The `ServerInfo` entity contains information about the configuration of
+the Gerrit server.
+
+[options="header",cols="1,^1,5"]
+|=======================================
+|Field Name                ||Description
+|`auth`                    ||
+Information about the authentication configuration as
+link:#auth-info[AuthInfo] entity.
+|`contact_store`           |optional|
+Information about the contact store configuration as
+link:#contact-store-info[ContactStoreInfo] entity.
+|`download`                ||
+Information about the configured download options as
+link:#download-info[DownloadInfo] entity.
+information about Gerrit
+|`gerrit`                  ||
+Information about the configuration from the
+link:config-gerrit.html#gerrit[gerrit] section as link:#gerrit-info[
+GerritInfo] entity.
+|=======================================
+
 [[summary-info]]
 === SummaryInfo
 The `SummaryInfo` entity contains information about the current state
diff --git a/Documentation/user-dashboards.txt b/Documentation/user-dashboards.txt
index f6db6cf..e64d625 100644
--- a/Documentation/user-dashboards.txt
+++ b/Documentation/user-dashboards.txt
@@ -1,5 +1,6 @@
 = Gerrit Code Review - Dashboards
 
+[[custom-dashboards]]
 == Custom Dashboards
 
 A custom dashboard is shown in a layout similar to the per-user
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
index 5ad6b39..a89d0db 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -44,10 +44,9 @@
 While in edit mode, it is possible to add new files to the change by clicking
 the 'Add...' button at the top of the file list.
 
-Files can be removed from the change, or restored, by clicking the icon to the
-left of the file name. Reverting a file in the change is also supported and is
-achieved in two steps: remove file from the change and restore the file in the
-change.
+File changes can be reverted or files can be removed from the change or
+deleted files can be restored, by clicking the icons to the left of the file
+name.
 
 To switch from edit mode back to review mode, click the 'Done Editing' button.
 
diff --git a/Documentation/user-named-queries.txt b/Documentation/user-named-queries.txt
new file mode 100644
index 0000000..0ddbbef
--- /dev/null
+++ b/Documentation/user-named-queries.txt
@@ -0,0 +1,32 @@
+= Gerrit Code Review - Named Queries
+
+[[user-named-queries]]
+== User Named Queries
+It is possible to define named queries on a user level. To do
+this, define the named queries in the `queries` file of
+the user's account ref in the `All-Users` project.  The user's
+account ref is based on the user's account id which is an
+integer.  The account refs are sharded by the last two digits
+(`+nn+`) in the refname, leading to refs of the format
+`+refs/users/nn/accountid+`.  The user's queries file is a
+2 column tab delimited file.  The left column represents the
+name of the query, and the right column represents the query
+expression represented by the name.
+
+Example queries file:
+
+----
+# Name         	Query
+#
+selfapproved   	owner:self label:code-review+2,user=self
+blocked        	label:code-review-2 OR label:verified-1
+# Note below how to reference your own named queries in other named queries
+ready          	label:code-review+2 label:verified+1 -query:blocked status:open
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 1f95d95..ca29e44 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -38,6 +38,7 @@
 |=============================================================
 
 
+[[search-operators]]
 == Search Operators
 
 Operators act as restrictions on the search.  As more operators
@@ -98,6 +99,12 @@
 +
 Changes originally submitted by a user in 'GROUP'.
 
+[[query]]
+query:'NAME'::
++
+Changes which match the current user's query named 'NAME'
+(see link:user-named-queries.html[Named Queries]).
+
 [[reviewer]]
 reviewer:'USER', r:'USER'::
 +
@@ -489,6 +496,7 @@
 of `draftby:self` will find changes where the caller has created
 a draft comment.
 
+[[limit]]
 limit:'CNT'::
 +
 Limit the returned results to no more than 'CNT' records.  This is
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index de9a2fb..25665a3 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -32,6 +32,7 @@
 and then following the site-specific instructions.  On sites where this URL is
 not configured, the password can be obtained by clicking on `Generate Password`.
 
+[[ssh]]
 == SSH
 
 Each user uploading changes to Gerrit must configure one or more SSH
@@ -145,6 +146,7 @@
 notify them of new changes will be automatically sent an email
 message when the push is completed.
 
+[[topic]]
 To include a short tag associated with all of the changes in the
 same group, such as the local topic branch name, append it after
 the destination branch name. In this example the short topic tag
diff --git a/ReleaseNotes/ReleaseNotes-2.10.1.txt b/ReleaseNotes/ReleaseNotes-2.10.1.txt
index 6ef9073..df70b64 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.1.txt
@@ -23,7 +23,7 @@
 ----
 
 * Several performance improvements when using LDAP, both in the number of LDAP
-requests and in the amount of data transfered.
+requests and in the amount of data transferred.
 
 * Sites using LDAP for authentication but otherwise rely on local Gerrit groups
 should set the new `ldap.fetchMemberOfEagerly` option to `false`.
diff --git a/ReleaseNotes/ReleaseNotes-2.10.3.txt b/ReleaseNotes/ReleaseNotes-2.10.3.txt
index 578a1ae..052840d 100644
--- a/ReleaseNotes/ReleaseNotes-2.10.3.txt
+++ b/ReleaseNotes/ReleaseNotes-2.10.3.txt
@@ -33,7 +33,7 @@
 +
 OpenID auth scheme is aware of optional OAuth2 plugin-based authentication.
 This feature is considered to be experimental and hasn't reached full feature set yet.
-Particularly, linking of user identities accross protocol boundaries and even from
+Particularly, linking of user identities across protocol boundaries and even from
 one OAuth2 identity to another OAuth2 identity wasn't implemented yet.
 
 Configuration
diff --git a/ReleaseNotes/ReleaseNotes-2.10.4.txt b/ReleaseNotes/ReleaseNotes-2.10.4.txt
new file mode 100644
index 0000000..e221549
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.10.4.txt
@@ -0,0 +1,49 @@
+Release notes for Gerrit 2.10.4
+===============================
+
+There are no schema changes from link:ReleaseNotes-2.10.3.1.html[2.10.3.1].
+
+Download:
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.10.4.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.10.4.war]
+
+New Features
+------------
+
+* Support identity linking in hybrid OpenID and OAuth2 authentication.
++
+Linking of user identities across protocol boundaries and from one OAuth2
+identity to another OAuth2 identity is supported.
+
+* Support identity linking in OAuth2 extension point.
++
+Linking of user identities from one OAuth2 identity to another OAuth2
+identity is supported.
+
+Bug Fixes
+---------
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3300[Issue 3300]:
+Fix >10x performance degradation for Git push and replication operations.
++
+A link:https://bugs.eclipse.org/bugs/show_bug.cgi?id=465509[regression in jgit]
+caused a performance degradation.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=3312[Issue 3312]:
+Flush padding on patches downloaded as base64.
++
+The padding was not flushed, which caused the downloaded patch to not be
+valid base64.
+
+OAuth extension point
+~~~~~~~~~~~~~~~~~~~~~
+
+* Check for session validity during logout.
++
+When user was trying to log out, after Gerrit restart, the session was
+invalidated and IllegalStateException was recorded in the error_log.
+
+Updates
+-------
+
+* Update jgit to 4.0.0.201505050340-m2.
diff --git a/ReleaseNotes/ReleaseNotes-2.11.1.txt b/ReleaseNotes/ReleaseNotes-2.11.1.txt
new file mode 100644
index 0000000..eab6d60
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.11.1.txt
@@ -0,0 +1,136 @@
+Release notes for Gerrit 2.11.1
+===============================
+
+Gerrit 2.11.1 is now available:
+
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.11.1.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.11.1.war]
+
+Gerrit 2.11.1 includes the bug fixes done with
+link:ReleaseNotes-2.10.4.html[Gerrit 2.10.4]. These bug fixes are *not* listed
+in these release notes.
+
+There are no schema changes from link:ReleaseNotes-2.11.html[2.11].
+
+
+New Features
+------------
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=321[Issue 321]:
+Use in-memory Lucene index for a better reviewer suggestion.
++
+Instead of a linear full text search through a list of accounts, use an
+in-memory Lucene index. The index is periodically refreshed. The refresh period
+is configurable via the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11.1/config-gerrit.html#suggest.fullTextSearchRefresh[
+suggest.fullTextSearchRefresh] parameter.
+
+
+Bug Fixes
+---------
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3363[Issue 3363]:
+Fix performance degrade in background mergeability checks.
++
+When neither `index.batchThreads` nor `changeMerge.threadPoolSize` was defined,
+the background mergeability check fell back to using an interactive executor.
++
+This led to a severe performance degradation during git push operations because
+the `ref-update` listener was reindexing all open changes on the target branch
+interactively. The degradation increased linearly with number of open changes on
+the target branch.
++
+Now, instead of indexing interactively, it falls back to a batch thread pool
+with the logical number of available CPUs.
+
+* Reduce unnecessary database access when queryng changes.
++
+Searching for changes was retrieving more information than necessary from the
+database. This has been optimized to reduce database access and make better use
+of the secondary index.
+
+* Fix `PatchLineCommentsUtil.draftByChangeAuthor`.
++
+There is not a native index for this, and the ReviewDb case was not properly
+filtering a result by change.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3323[Issue 3323]:
+Fix internal server error when cloning from a slave while hiding some refs.
+
+* Require 'View Plugins' capability to list plugins through SSH.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3191[Issue 3191]:
+Always show 'Not Current' as state when looking at old patch set.
++
+For merged changes it was confusing for users to see the status as 'Merged' when
+they look at an old patch set.
+
+* Fix project creation with plugin config if user is not project owner.
++
+On project creation it is possible to specify plugin configuration values that
+should be stored in the `project.config` file. This failed if the calling user
+was not becoming owner of the created project, because only project owners can
+edit the `project.config` file.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3342[Issue 3342]:
+Log `IOException` on failure to update project configuration.
++
+Without logging these exceptions it's hard to guess why the update of the
+project configuration is failing.
+
+* Don't show stack trace when failing to build BloomFilter during reindex.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3337[Issue 3337]:
+Reenable 'Revert' button when revert is cancelled.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3325[Issue 3325]:
+Add missing `--newrev` parameter to the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11.1/config-hooks.html#_change_merged[
+change-merged hook documentation].
+
+* Fix `gc_log` when running in a web container.
++
+All logs supposed to be in the `gc_log` file were ending up in the main log
+instead when deploying Gerrit in a web container.
+
+* Fix binding of SecureStore modules.
++
+The SecureStore modules were not correctly added when Gerrit was deployed in a
+web container with the site path configured using the `gerrit.site_path`
+property.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3310[Issue 3310]:
+Fix disabling plugins when Gerrit is running on Windows.
++
+When running Gerrit on Windows it was not possible to disable a plugin due to an
+error renaming the plugin's JAR file.
+
+* Remove temporary GitWeb config on Gerrit exit.
++
+A temporary directory was being created but not removed.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3346[Issue 3346]:
+Fix typo in the
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11.1/config-reverseproxy.html[
+Apache 2 configuration documentation].
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=3346[Issue 3346]:
+Fix incorrect documentatation of
+link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.11.1/config-gerrit.html#auth.registerUrl[
+auth types].
+
+* Replication plugin
+
+** Fix creation of missing repositories.
++
+Missing projects were not being created on the destination.
+
+** Emit replication status events after initial full sync.
++
+When `replicateOnStartup` is enabled, the plugin was not emitting the status
+events after the initial sync.
+
+Updates
+-------
+
+* Update CodeMirror to 5.0.
diff --git a/ReleaseNotes/ReleaseNotes-2.11.txt b/ReleaseNotes/ReleaseNotes-2.11.txt
index 3bc6c96..90519dc 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.txt
@@ -486,7 +486,7 @@
 * Plugins can provide project-aware top menu extensions
 +
 Plugins can provide sub-menu items within the 'Projects' context. The
-'${projectName}' placeholder is replaced by the project name.
+'$\{projectName\}' placeholder is replaced by the project name.
 
 * Auto register static/init.js as JavaScript plugin.
 +
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
index cdef554..4abed47 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -923,11 +923,11 @@
 working tree dirty.  Eclipse 4 (Juno) still overwrites these files but
 doesn't write the timestamp.  This should help to keep the working tree
 clean.  However, since the timestamp is currently present in these
-files, Eclispe 4 would still make them dirty by overwriting and
+files, Eclipse 4 would still make them dirty by overwriting and
 effectively removing the timestamp.
 +
 This change removes the timestamp from these files. This helps those
-using Eclipse 4 and doesn't make it worse for those still using Eclispe
+using Eclipse 4 and doesn't make it worse for those still using Eclipse
 3.
 
 * Add Maven profile to skip build of plugin modules
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
index bc854a2..92cdda2 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -625,7 +625,7 @@
 
 * Do not persist default project state in `project.config`.
 
-* Honor the `gerrit.cannonicalWebUrl` setting when opening the browser after init.
+* Honor the `gerrit.canonicalWebUrl` setting when opening the browser after init.
 
 * Fix 'query disabled' error when Query Limit is set.
 
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 99db8fb..0ee2a8d 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -4,11 +4,13 @@
 [[2_11]]
 Version 2.11.x
 --------------
+* link:ReleaseNotes-2.11.1.html[2.11.1]
 * link:ReleaseNotes-2.11.html[2.11]
 
 [[2_10]]
 Version 2.10.x
 --------------
+* link:ReleaseNotes-2.10.4.html[2.10.4]
 * link:ReleaseNotes-2.10.3.1.html[2.10.3.1]
 * link:ReleaseNotes-2.10.3.html[2.10.3]
 * link:ReleaseNotes-2.10.2.html[2.10.2]
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index f747fd3..0b40891 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -38,10 +38,12 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -132,12 +134,19 @@
   protected Provider<InternalChangeQuery> queryProvider;
 
   @Inject
+  @CanonicalWebUrl
+  protected Provider<String> canonicalWebUrl;
+
+  @Inject
   @GerritServerConfig
   protected Config cfg;
 
   @Inject
   private InProcessProtocol inProcessProtocol;
 
+  @Inject
+  private Provider<AnonymousUser> anonymousUser;
+
   protected TestRepository<InMemoryRepository> testRepo;
   protected GerritServer server;
   protected TestAccount admin;
@@ -422,6 +431,10 @@
     return atrScope.set(newRequestContext(account));
   }
 
+  protected Context setApiUserAnonymous() {
+    return atrScope.newContext(reviewDbProvider, null, anonymousUser.get());
+  }
+
   protected static Gson newGson() {
     return OutputFormat.JSON_COMPACT.newGson();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
index 61a8191..0ff4709 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -162,6 +162,21 @@
     return old;
   }
 
+  public Context disableDb() {
+    Context old = current.get();
+    SchemaFactory<ReviewDb> sf = new SchemaFactory<ReviewDb>() {
+      @Override
+      public ReviewDb open() {
+        return new DisabledReviewDb();
+      }
+    };
+    Context ctx = new Context(sf, old.session, old.user, old.created);
+
+    current.set(ctx);
+    local.setContext(ctx);
+    return old;
+  }
+
   /** Returns exactly one instance per command executed. */
   static final Scope REQUEST = new Scope() {
     @Override
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
index 07d0f50..b07ed30 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
@@ -15,11 +15,13 @@
 package com.google.gerrit.acceptance;
 
 import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 
 import org.eclipse.jgit.lib.Config;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 
 class ConfigAnnotationParser {
   private static Splitter splitter = Splitter.on(".").trimResults();
@@ -45,9 +47,19 @@
   private static void parseAnnotation(Config cfg, GerritConfig c) {
     ArrayList<String> l = Lists.newArrayList(splitter.split(c.name()));
     if (l.size() == 2) {
-      cfg.setString(l.get(0), null, l.get(1), c.value());
+      if (!Strings.isNullOrEmpty(c.value())) {
+        cfg.setString(l.get(0), null, l.get(1), c.value());
+      } else {
+        String[] values = c.values();
+        cfg.setStringList(l.get(0), null, l.get(1), Arrays.asList(values));
+      }
     } else if (l.size() == 3) {
-      cfg.setString(l.get(0), l.get(1), l.get(2), c.value());
+      if (!Strings.isNullOrEmpty(c.value())) {
+        cfg.setString(l.get(0), l.get(1), l.get(2), c.value());
+      } else {
+        cfg.setStringList(l.get(0), l.get(1), l.get(2),
+            Arrays.asList(c.value()));
+      }
     } else {
       throw new IllegalArgumentException(
           "GerritConfig.name must be of the format"
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/DisabledReviewDb.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/DisabledReviewDb.java
new file mode 100644
index 0000000..44d3d7f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/DisabledReviewDb.java
@@ -0,0 +1,206 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.reviewdb.server.AccountAccess;
+import com.google.gerrit.reviewdb.server.AccountDiffPreferenceAccess;
+import com.google.gerrit.reviewdb.server.AccountExternalIdAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupByIdAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupByIdAudAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupMemberAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupMemberAuditAccess;
+import com.google.gerrit.reviewdb.server.AccountGroupNameAccess;
+import com.google.gerrit.reviewdb.server.AccountPatchReviewAccess;
+import com.google.gerrit.reviewdb.server.AccountProjectWatchAccess;
+import com.google.gerrit.reviewdb.server.AccountSshKeyAccess;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
+import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
+import com.google.gerrit.reviewdb.server.PatchSetAccess;
+import com.google.gerrit.reviewdb.server.PatchSetAncestorAccess;
+import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
+import com.google.gerrit.reviewdb.server.StarredChangeAccess;
+import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
+import com.google.gerrit.reviewdb.server.SystemConfigAccess;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.StatementExecutor;
+
+/** ReviewDb that is disabled for testing. */
+class DisabledReviewDb implements ReviewDb {
+  private static final String MESSAGE = "ReviewDb is disabled for this test";
+
+  @Override
+  public void close() {
+    // Do nothing.
+  }
+
+  @Override
+  public void commit() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public void rollback() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public void updateSchema(StatementExecutor e) {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public void pruneSchema(StatementExecutor e) {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public Access<?, ?>[] allRelations() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public SchemaVersionAccess schemaVersion() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public SystemConfigAccess systemConfig() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountAccess accounts() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountExternalIdAccess accountExternalIds() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountSshKeyAccess accountSshKeys() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupAccess accountGroups() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupNameAccess accountGroupNames() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupMemberAccess accountGroupMembers() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountDiffPreferenceAccess accountDiffPreferences() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public StarredChangeAccess starredChanges() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountProjectWatchAccess accountProjectWatches() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountPatchReviewAccess accountPatchReviews() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public ChangeAccess changes() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public PatchSetApprovalAccess patchSetApprovals() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public ChangeMessageAccess changeMessages() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public PatchSetAccess patchSets() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public PatchSetAncestorAccess patchSetAncestors() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public PatchLineCommentAccess patchComments() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public SubmoduleSubscriptionAccess submoduleSubscriptions() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupByIdAccess accountGroupById() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public AccountGroupByIdAudAccess accountGroupByIdAud() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public int nextAccountId() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public int nextAccountGroupId() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public int nextChangeId() {
+    throw new AssertionError(MESSAGE);
+  }
+
+  @Override
+  public int nextChangeMessageId() {
+    throw new AssertionError(MESSAGE);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
index 5cb1229..4b956a2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritConfig.java
@@ -24,5 +24,6 @@
 @Retention(RUNTIME)
 public @interface GerritConfig {
   String name();
-  String value();
+  String value() default "";
+  String[] values() default "";
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index aed5bd6..a721221 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -190,7 +190,8 @@
     cfg.setBoolean("sshd", null, "testUseInsecureRandom", true);
     cfg.setString("cache", null, "directory", null);
     cfg.setString("gerrit", null, "basePath", "git");
-    cfg.setBoolean("sendemail", null, "enable", false);
+    cfg.setBoolean("sendemail", null, "enable", true);
+    cfg.setInt("sendemail", null, "threadPoolSize", 0);
     cfg.setInt("cache", "projects", "checkFrequency", 0);
     cfg.setInt("plugins", null, "checkFrequency", 0);
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java
index 29185a85..e8f8925 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GitUtil.java
@@ -152,24 +152,6 @@
     return Iterables.getOnlyElement(r);
   }
 
-  public static class Commit {
-    private final RevCommit commit;
-    private final String changeId;
-
-    Commit(RevCommit commit, String changeId) {
-      this.commit = commit;
-      this.changeId = changeId;
-    }
-
-    public RevCommit getCommit() {
-      return commit;
-    }
-
-    public String getChangeId() {
-      return changeId;
-    }
-  }
-
   public static Optional<String> getChangeId(TestRepository<?> tr, ObjectId id)
       throws IOException {
     RevCommit c = tr.getRevWalk().parseCommit(id);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
index 701b337..794f832 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
@@ -42,11 +42,12 @@
   }
 
   @SuppressWarnings("resource")
-  public String exec(String command) throws JSchException, IOException {
+  public String exec(String command, InputStream opt) throws JSchException,
+      IOException {
     ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
     try {
       channel.setCommand(command);
-      channel.setInputStream(null);
+      channel.setInputStream(opt);
       InputStream in = channel.getInputStream();
       channel.connect();
 
@@ -60,6 +61,20 @@
     }
   }
 
+  public InputStream exec2(String command, InputStream opt) throws JSchException,
+      IOException {
+    ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
+    channel.setCommand(command);
+    channel.setInputStream(opt);
+    InputStream in = channel.getInputStream();
+    channel.connect();
+    return in;
+  }
+
+  public String exec(String command) throws JSchException, IOException {
+    return exec(command, null);
+  }
+
   public boolean hasError() {
     return error != null;
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index f084b98..aaf6192 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -67,12 +67,12 @@
     String adminUsername = "admin";
     List<AccountInfo> result = gApi.accounts()
         .suggestAccounts().withQuery(adminUsername).get();
-    assertThat(result.size()).is(1);
+    assertThat(result).hasSize(1);
     assertThat(result.get(0).username).isEqualTo(adminUsername);
 
     List<AccountInfo> resultShortcutApi = gApi.accounts()
         .suggestAccounts(adminUsername).get();
-    assertThat(resultShortcutApi.size()).is(result.size());
+    assertThat(resultShortcutApi).hasSize(result.size());
 
     List<AccountInfo> emptyResult = gApi.accounts()
         .suggestAccounts("unknown").get();
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 f87349a..9a58702 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
@@ -15,6 +15,11 @@
 package com.google.gerrit.acceptance.api.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.project.Util.category;
+import static com.google.gerrit.server.project.Util.value;
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -22,6 +27,8 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -33,8 +40,12 @@
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
 
 import org.eclipse.jgit.lib.Constants;
 import org.junit.Test;
@@ -60,9 +71,9 @@
     assertThat(c.mergeable).isTrue();
     assertThat(c.changeId).isEqualTo(r.getChangeId());
     assertThat(c.created).isEqualTo(c.updated);
-    assertThat(c._number).is(r.getChange().getId().get());
+    assertThat(c._number).isEqualTo(r.getChange().getId().get());
 
-    assertThat(c.owner._accountId).is(admin.getId().get());
+    assertThat(c.owner._accountId).isEqualTo(admin.getId().get());
     assertThat(c.owner.name).isNull();
     assertThat(c.owner.email).isNull();
     assertThat(c.owner.username).isNull();
@@ -128,7 +139,7 @@
         .revision(r3.getCommit().name())
         .rebase(ri);
     PatchSet ps3 = r3.getPatchSet();
-    assertThat(ps3.getId().get()).is(2);
+    assertThat(ps3.getId().get()).isEqualTo(2);
 
     // rebase r2 onto r3 (referenced by ref)
     ri.base = ps3.getId().toRefName();
@@ -137,7 +148,7 @@
         .revision(r2.getCommit().name())
         .rebase(ri);
     PatchSet ps2 = r2.getPatchSet();
-    assertThat(ps2.getId().get()).is(2);
+    assertThat(ps2.getId().get()).isEqualTo(2);
 
     // rebase r1 onto r2 (referenced by commit)
     ri.base = ps2.getRevision().get();
@@ -146,7 +157,7 @@
         .revision(r1.getCommit().name())
         .rebase(ri);
     PatchSet ps1 = r1.getPatchSet();
-    assertThat(ps1.getId().get()).is(2);
+    assertThat(ps1.getId().get()).isEqualTo(2);
 
     // rebase r1 onto r3 (referenced by change number)
     ri.base = String.valueOf(r3.getChange().getId().get());
@@ -154,7 +165,7 @@
         .id(r1.getChangeId())
         .revision(ps1.getRevision().get())
         .rebase(ri);
-    assertThat(r1.getPatchSetId().get()).is(3);
+    assertThat(r1.getPatchSetId().get()).isEqualTo(3);
   }
 
   @Test(expected = ResourceConflictException.class)
@@ -190,7 +201,7 @@
         .id(r.getChangeId())
         .addReviewer(in);
 
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
         .containsExactlyElementsIn(ImmutableSet.of(user.id));
   }
 
@@ -206,7 +217,7 @@
         .revision(r.getCommit().name())
         .submit();
 
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
       .containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
 
     AddReviewerInput in = new AddReviewerInput();
@@ -214,7 +225,7 @@
     gApi.changes()
         .id(r.getChangeId())
         .addReviewer(in);
-    assertThat((Iterable<?>)getReviewers(r.getChangeId()))
+    assertThat(getReviewers(r.getChangeId()))
         .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.id));
   }
 
@@ -293,7 +304,7 @@
     PushOneCommit.Result r = createChange();
     ChangeInfo result = Iterables.getOnlyElement(query(r.getChangeId()));
     assertThat(result.labels).isNull();
-    assertThat((Iterable<?>)result.messages).isNull();
+    assertThat(result.messages).isNull();
     assertThat(result.revisions).isNull();
     assertThat(result.actions).isNull();
   }
@@ -307,13 +318,13 @@
         .get());
     assertThat(Iterables.getOnlyElement(result.labels.keySet()))
         .isEqualTo("Code-Review");
-    assertThat((Iterable<?>)result.messages).hasSize(1);
+    assertThat(result.messages).hasSize(1);
     assertThat(result.actions).isNotEmpty();
 
     RevisionInfo rev = Iterables.getOnlyElement(result.revisions.values());
     assertThat(rev._number).isEqualTo(r.getPatchSetId().get());
     assertThat(rev.created).isNotNull();
-    assertThat(rev.uploader._accountId).is(admin.getId().get());
+    assertThat(rev.uploader._accountId).isEqualTo(admin.getId().get());
     assertThat(rev.ref).isEqualTo(r.getPatchSetId().toRefName());
     assertThat(rev.actions).isNotEmpty();
   }
@@ -376,4 +387,86 @@
         .get(EnumSet.of(ListChangesOption.CHECK))
         .problems).isEmpty();
   }
+
+  @Test
+  public void commitFooters() throws Exception {
+    LabelType verified = category("Verified",
+        value(1, "Failed"), value(0, "No score"), value(-1, "Passes"));
+    LabelType custom1 = category("Custom1",
+        value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+    LabelType custom2 = category("Custom2",
+        value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
+    ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+    cfg.getLabelSections().put(verified.getName(), verified);
+    cfg.getLabelSections().put(custom1.getName(), verified);
+    cfg.getLabelSections().put(custom2.getName(), verified);
+    String heads = "refs/heads/*";
+    AccountGroup.UUID anon =
+        SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+    Util.allow(cfg, Permission.forLabel("Verified"), -1, 1, anon, heads);
+    Util.allow(cfg, Permission.forLabel("Custom1"), -1, 1, anon, heads);
+    Util.allow(cfg, Permission.forLabel("Custom2"), -1, 1, anon, heads);
+    saveProjectConfig(project, cfg);
+
+    PushOneCommit.Result r1 = createChange();
+    r1.assertOkStatus();
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content",
+          r1.getChangeId())
+        .to("refs/for/master");
+    r2.assertOkStatus();
+
+    ReviewInput in = new ReviewInput();
+    in.label("Code-Review", 1);
+    in.label("Verified", 1);
+    in.label("Custom1", -1);
+    in.label("Custom2", 1);
+    gApi.changes().id(r2.getChangeId()).current().review(in);
+
+    EnumSet<ListChangesOption> options = EnumSet.of(
+        ListChangesOption.ALL_REVISIONS, ListChangesOption.COMMIT_FOOTERS);
+    ChangeInfo actual = gApi.changes().id(r2.getChangeId()).get(options);
+    assertThat(actual.revisions).hasSize(2);
+
+    // No footers except on latest patch set.
+    assertThat(actual.revisions.get(r1.getCommit().getName()).commitWithFooters)
+        .isNull();
+
+    String expected = SUBJECT + "\n"
+        + "\n"
+        + "Change-Id: " + r2.getChangeId() + "\n"
+        + "Reviewed-on: "
+            + canonicalWebUrl.get() + r2.getChange().getId() + "\n"
+        + "Reviewed-by: Administrator <admin@example.com>\n"
+        + "Custom2: Administrator <admin@example.com>\n"
+        + "Tested-by: Administrator <admin@example.com>\n";
+    assertThat(actual.revisions.get(r2.getCommit().getName()).commitWithFooters)
+        .isEqualTo(expected);
+  }
+
+  @Test
+  public void defaultSearchDoesNotTouchDatabase() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    gApi.changes()
+        .id(r1.getChangeId())
+        .revision(r1.getCommit().name())
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(r1.getChangeId())
+        .revision(r1.getCommit().name())
+        .submit();
+
+    createChange();
+
+    setApiUserAnonymous(); // Identified user may async get stars from DB.
+    atrScope.disableDb();
+    assertThat(gApi.changes().query()
+          .withQuery(
+            "project:{" + project.get() + "} (status:open OR status:closed)")
+          // Options should match defaults in ChangeTable.
+          .withOption(ListChangesOption.LABELS)
+          .withOption(ListChangesOption.DETAILED_ACCOUNTS)
+          .get())
+        .hasSize(2);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
index e90b28e..8ed4165 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -387,7 +387,7 @@
             }
           }).toSortedList(Ordering.natural());
     assertThat(expectedGroups.size()).isAtLeast(2);
-    assertThat((Iterable<?>) gApi.groups().list().getAsMap().keySet())
+    assertThat(gApi.groups().list().getAsMap().keySet())
         .containsExactlyElementsIn(expectedGroups).inOrder();
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 1ebae60..0d46ed7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -153,10 +153,10 @@
     ChangeApi orig = gApi.changes()
         .id(project.get() + "~master~" + r.getChangeId());
 
-    assertThat((Iterable<?>)orig.get().messages).hasSize(1);
+    assertThat(orig.get().messages).hasSize(1);
     ChangeApi cherry = orig.revision(r.getCommit().name())
         .cherryPick(in);
-    assertThat((Iterable<?>)orig.get().messages).hasSize(2);
+    assertThat(orig.get().messages).hasSize(2);
 
     String cherryPickedRevision = cherry.get().currentRevision;
     String expectedMessage = String.format(
@@ -168,13 +168,33 @@
     origIt.next();
     assertThat(origIt.next().message).isEqualTo(expectedMessage);
 
-    assertThat((Iterable<?>)cherry.get().messages).hasSize(1);
+    assertThat(cherry.get().messages).hasSize(1);
     Iterator<ChangeMessageInfo> cherryIt = cherry.get().messages.iterator();
     expectedMessage = "Patch Set 1: Cherry Picked from branch master.";
     assertThat(cherryIt.next().message).isEqualTo(expectedMessage);
 
     assertThat(cherry.get().subject).contains(in.message);
-    assertThat(cherry.get().topic).isEqualTo("someTopic");
+    assertThat(cherry.get().topic).isEqualTo("someTopic-foo");
+    cherry.current().review(ReviewInput.approve());
+    cherry.current().submit();
+  }
+
+  @Test
+  public void cherryPickwithNoTopic() throws Exception {
+    PushOneCommit.Result r = pushTo("refs/for/master");
+    CherryPickInput in = new CherryPickInput();
+    in.destination = "foo";
+    in.message = "it goes to stable branch";
+    gApi.projects()
+        .name(project.get())
+        .branch(in.destination)
+        .create(new BranchInput());
+    ChangeApi orig = gApi.changes()
+        .id(project.get() + "~master~" + r.getChangeId());
+
+    ChangeApi cherry = orig.revision(r.getCommit().name())
+        .cherryPick(in);
+    assertThat(cherry.get().topic).isNull();
     cherry.current().review(ReviewInput.approve());
     cherry.current().submit();
   }
@@ -190,7 +210,7 @@
         .revision(r.getCommit().name())
         .cherryPick(in)
         .get();
-    assertThat((Iterable<?>)cherryInfo.messages).hasSize(2);
+    assertThat(cherryInfo.messages).hasSize(2);
     Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
     assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
     assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
@@ -228,7 +248,7 @@
     in.message = subject;
     ChangeApi cherry = orig.revision(r2.getCommit().name()).cherryPick(in);
     ChangeInfo cherryInfo = cherry.get();
-    assertThat((Iterable<?>)cherryInfo.messages).hasSize(2);
+    assertThat(cherryInfo.messages).hasSize(2);
     Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
     assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 1.");
     assertThat(cherryIt.next().message).isEqualTo("Uploaded patch set 2.");
@@ -253,10 +273,10 @@
     ChangeApi orig = gApi.changes()
         .id(project.get() + "~master~" + r.getChangeId());
 
-    assertThat((Iterable<?>)orig.get().messages).hasSize(1);
+    assertThat(orig.get().messages).hasSize(1);
     ChangeApi cherry = orig.revision(r.getCommit().name())
         .cherryPick(in);
-    assertThat((Iterable<?>)orig.get().messages).hasSize(2);
+    assertThat(orig.get().messages).hasSize(2);
 
     assertThat(cherry.get().subject).contains(in.message);
     cherry.current().review(ReviewInput.approve());
@@ -288,7 +308,7 @@
 
     String triplet = project.get() + "~master~" + r.getChangeId();
     ChangeApi orig = gApi.changes().id(triplet);
-    assertThat((Iterable<?>)orig.get().messages).hasSize(1);
+    assertThat(orig.get().messages).hasSize(1);
 
     try {
       orig.revision(r.getCommit().name()).cherryPick(in);
@@ -345,7 +365,7 @@
         .current()
         .setReviewed(PushOneCommit.FILE_NAME, false);
 
-    assertThat((Iterable<?>)gApi.changes().id(r.getChangeId()).current().reviewed())
+    assertThat(gApi.changes().id(r.getChangeId()).current().reviewed())
         .isEmpty();
   }
 
@@ -505,6 +525,19 @@
     CommentInfo comment = Iterables.getOnlyElement(out.get(FILE_NAME));
     assertThat(comment.message).isEqualTo(in.message);
     assertThat(comment.author.email).isEqualTo(admin.email);
+    assertThat(comment.path).isNull();
+
+    List<CommentInfo> list = gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .commentsAsList();
+    assertThat(list).hasSize(1);
+
+    CommentInfo comment2 = list.get(0);
+    assertThat(comment2.path).isEqualTo(FILE_NAME);
+    assertThat(comment2.line).isEqualTo(comment.line);
+    assertThat(comment2.message).isEqualTo(comment.message);
+    assertThat(comment2.author.email).isEqualTo(comment.author.email);
 
     assertThat(gApi.changes()
         .id(r.getChangeId())
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 4007e32..82f8c59 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -456,6 +456,30 @@
   }
 
   @Test
+  public void revertChanges() throws Exception {
+    assertThat(modifier.createEdit(change2, ps2)).isEqualTo(
+        RefUpdate.Result.NEW);
+    Optional<ChangeEdit> edit = editUtil.byChange(change2);
+    assertThat(modifier.restoreFile(edit.get(), FILE_NAME)).isEqualTo(
+        RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change2);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
+    assertThat(
+        modifier.modifyFile(editUtil.byChange(change2).get(), FILE_NAME,
+            RestSession.newRawInput(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change2);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_NEW);
+    assertThat(modifier.restoreFile(edit.get(), FILE_NAME)).isEqualTo(
+        RefUpdate.Result.FORCED);
+    edit = editUtil.byChange(change2);
+    assertByteArray(fileUtil.getContent(projectCache.get(edit.get().getChange().getProject()),
+        ObjectId.fromString(edit.get().getRevision().get()), FILE_NAME), CONTENT_OLD);
+    editUtil.delete(edit.get());
+  }
+
+  @Test
   public void renameFileRest() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
     Post.Input in = new Post.Input();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 880ad29..2323270 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -175,7 +175,7 @@
     LabelInfo cr = ci.labels.get("Code-Review");
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).name).isEqualTo("Administrator");
-    assertThat(cr.all.get(0).value).is(1);
+    assertThat(cr.all.get(0).value).isEqualTo(1);
 
     PushOneCommit push =
         pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
@@ -186,7 +186,7 @@
     cr = ci.labels.get("Code-Review");
     assertThat(cr.all).hasSize(1);
     assertThat(cr.all.get(0).name).isEqualTo("Administrator");
-    assertThat(cr.all.get(0).value).is(2);
+    assertThat(cr.all.get(0).value).isEqualTo(2);
   }
 
   @Test
@@ -233,7 +233,7 @@
     r.assertChange(Change.Status.NEW, null);
 
     Set<String> hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
-    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+    assertThat(hashtags).containsExactlyElementsIn(expected);
 
     // specify a single hashtag as option in new patch set
     String hashtag2 = "tag2";
@@ -244,7 +244,7 @@
     r.assertOkStatus();
     expected = ImmutableSet.of(hashtag1, hashtag2);
     hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
-    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+    assertThat(hashtags).containsExactlyElementsIn(expected);
   }
 
   @Test
@@ -263,7 +263,7 @@
     r.assertChange(Change.Status.NEW, null);
 
     Set<String> hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
-    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+    assertThat(hashtags).containsExactlyElementsIn(expected);
 
     // specify multiple hashtags as options in new patch set
     String hashtag3 = "tag3";
@@ -275,7 +275,7 @@
     r.assertOkStatus();
     expected = ImmutableSet.of(hashtag1, hashtag2, hashtag3, hashtag4);
     hashtags = gApi.changes().id(r.getChangeId()).getHashtags();
-    assertThat((Iterable<?>)hashtags).containsExactlyElementsIn(expected);
+    assertThat(hashtags).containsExactlyElementsIn(expected);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 207c6b1..5066e66 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -253,7 +253,7 @@
     try (Repository r = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(r)) {
       RevCommit c = rw.parseCommit(r.getRef(branch).getObjectId());
-      assertThat(c.getParentCount()).is(2);
+      assertThat(c.getParentCount()).isEqualTo(2);
       assertThat(c.getShortMessage()).isEqualTo("Merge \"" + subject + "\"");
       assertThat(c.getAuthorIdent().getEmailAddress()).isEqualTo(admin.email);
       assertThat(c.getCommitterIdent().getEmailAddress()).isEqualTo(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
index d444308..3ffe07c 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsIT.java
@@ -73,6 +73,27 @@
   }
 
   @Test
+  public void testSubscriptionUnsubscribeByDeletingGitModules() throws Exception {
+    TestRepository<?> superRepo = createProjectWithPush("super-project");
+    TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
+
+    pushChangeTo(subRepo, "master");
+    createSubscription(superRepo, "master", "subscribed-to-project", "master");
+
+    pushChangeTo(subRepo, "master");
+    ObjectId subHEADbeforeUnsubscribing = pushChangeTo(subRepo, "master");
+
+    deleteGitModulesFile(superRepo, "master");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEADbeforeUnsubscribing);
+
+    pushChangeTo(superRepo, "master", "commit after unsubscribe");
+    pushChangeTo(subRepo, "master", "commit after unsubscribe");
+    expectToHaveSubmoduleState(superRepo, "master",
+        "subscribed-to-project", subHEADbeforeUnsubscribing);
+  }
+
+  @Test
   public void testSubscriptionToDifferentBranches() throws Exception {
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> subRepo = createProjectWithPush("subscribed-to-project");
@@ -141,6 +162,23 @@
     assertThat(actualId).isEqualTo(expectedId);
   }
 
+  private void deleteGitModulesFile(TestRepository<?> repo, String branch)
+      throws Exception {
+    repo.git().fetch().setRemote("origin").call();
+    repo.reset("refs/remotes/origin/" + branch);
+
+    ObjectId expectedId = repo.branch("HEAD").commit().insertChangeId()
+      .message("delete .gitmodules")
+      .rm(".gitmodules")
+      .create();
+    repo.git().push().setRemote("origin").setRefSpecs(
+      new RefSpec("HEAD:refs/heads/" + branch)).call();
+
+    ObjectId actualId = repo.git().fetch().setRemote("origin").call()
+      .getAdvertisedRef("refs/heads/master").getObjectId();
+    assertThat(actualId).isEqualTo(expectedId);
+  }
+
   private boolean hasSubmodule(TestRepository<?> repo, String branch,
       String submodule) throws Exception {
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 85037f1..7823e7d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 
-import com.google.gerrit.acceptance.git.AbstractSubmoduleSubscription;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.testutil.ConfigSuite;
@@ -55,7 +54,6 @@
     RevCommit c1 = subRepo.branch("HEAD").commit().insertChangeId()
       .message("first change")
       .add("asdf", "asdf\n")
-      .parent(c)
       .create();
     subRepo.git().push().setRemote("origin")
       .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
@@ -65,13 +63,11 @@
     RevCommit c2 = subRepo.branch("HEAD").commit().insertChangeId()
       .message("qwerty")
       .add("qwerty", "qwerty")
-      .parent(c)
       .create();
 
     RevCommit c3 = subRepo.branch("HEAD").commit().insertChangeId()
       .message("qwerty followup")
       .add("qwerty", "qwerty\nqwerty\n")
-      .parent(c2)
       .create();
     subRepo.git().push().setRemote("origin")
       .setRefSpecs(new RefSpec("HEAD:refs/for/master/" + name("topic-foo")))
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
index f52effd..0b07b87 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/VisibleRefFilterIT.java
@@ -280,7 +280,7 @@
     }
 
     Splitter s = Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings();
-    assertThat(filtered).containsSequence(
-        Ordering.natural().sortedCopy(s.split(out)));
+    assertThat(filtered).containsExactlyElementsIn(
+        Ordering.natural().sortedCopy(s.split(out))).inOrder();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index e6c5e97..a07bde7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -152,8 +152,8 @@
 
   private void assertSubmitter(PushOneCommit.Result change) throws Exception {
     ChangeInfo info = get(change.getChangeId(), ListChangesOption.MESSAGES);
-    assertThat((Iterable<?>)info.messages).isNotNull();
-    assertThat((Iterable<?>)info.messages).hasSize(3);
+    assertThat(info.messages).isNotNull();
+    assertThat(info.messages).hasSize(3);
     if (getSubmitType() == SubmitType.CHERRY_PICK) {
       assertThat(Iterables.getLast(info.messages).message).startsWith(
           "Change has been successfully cherry-picked as ");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
index 3c76355..a56a7f2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeMessagesIT.java
@@ -73,15 +73,15 @@
     String changeId = createChange().getChangeId();
     postMessage(changeId, "Some nits need to be fixed.");
     ChangeInfo c = info(changeId);
-    assertThat((Iterable<?>)c.messages).isNull();
+    assertThat(c.messages).isNull();
   }
 
   @Test
   public void defaultMessage() throws Exception {
     String changeId = createChange().getChangeId();
     ChangeInfo c = get(changeId);
-    assertThat((Iterable<?>)c.messages).isNotNull();
-    assertThat((Iterable<?>)c.messages).hasSize(1);
+    assertThat(c.messages).isNotNull();
+    assertThat(c.messages).hasSize(1);
     assertThat(c.messages.iterator().next().message)
       .isEqualTo("Uploaded patch set 1.");
   }
@@ -94,8 +94,8 @@
     String secondMessage = "I like this feature.";
     postMessage(changeId, secondMessage);
     ChangeInfo c = get(changeId);
-    assertThat((Iterable<?>)c.messages).isNotNull();
-    assertThat((Iterable<?>)c.messages).hasSize(3);
+    assertThat(c.messages).isNotNull();
+    assertThat(c.messages).hasSize(3);
     Iterator<ChangeMessageInfo> it = c.messages.iterator();
     assertThat(it.next().message).isEqualTo("Uploaded patch set 1.");
     assertMessage(firstMessage, it.next().message);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
index 74ea586..47d071f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -71,7 +71,7 @@
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
     RestResponse r = deletePatchSet(changeId, ps, adminSession);
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
-    assertThat(getChange(changeId).patches().size()).isEqualTo(1);
+    assertThat(getChange(changeId).patchSets()).hasSize(1);
     ps = getCurrentPatchSet(changeId);
     r = deletePatchSet(changeId, ps, adminSession);
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index 5f46312..6ef53ff 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -196,7 +196,7 @@
         ? extends IterableSubject<?, String, Iterable<String>>,
         String, Iterable<String>>
       assertThatGet(PushOneCommit.Result r) throws Exception {
-    return assertThat((Iterable<String>) gApi.changes()
+    return assertThat(gApi.changes()
         .id(r.getChange().getId().get())
         .getHashtags());
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
index 63e5627..3728a51 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ListChangesOptionsIT.java
@@ -68,7 +68,7 @@
   public void currentRevision() throws Exception {
     ChangeInfo c = get(changeId, CURRENT_REVISION);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat((Iterable<?>)c.revisions.keySet()).containsAllIn(
+    assertThat(c.revisions.keySet()).containsAllIn(
         ImmutableSet.of(commitId(2)));
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
@@ -78,7 +78,7 @@
     ChangeInfo c = get(changeId, CURRENT_REVISION, MESSAGES);
     assertThat(c.revisions).hasSize(1);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat((Iterable<?>)c.revisions.keySet()).containsAllIn(
+    assertThat(c.revisions.keySet()).containsAllIn(
         ImmutableSet.of(commitId(2)));
     assertThat(c.revisions.get(commitId(2))._number).isEqualTo(3);
   }
@@ -87,7 +87,7 @@
   public void allRevisions() throws Exception {
     ChangeInfo c = get(changeId, ALL_REVISIONS);
     assertThat(c.currentRevision).isEqualTo(commitId(2));
-    assertThat((Iterable<?>)c.revisions.keySet()).containsAllIn(
+    assertThat(c.revisions.keySet()).containsAllIn(
         ImmutableSet.of(commitId(0), commitId(1), commitId(2)));
     assertThat(c.revisions.get(commitId(0))._number).isEqualTo(1);
     assertThat(c.revisions.get(commitId(1))._number).isEqualTo(2);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index d0627f6..ed6740a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -62,9 +62,9 @@
     group2 = group("users2");
     group3 = group("users3");
 
-    user1 = user("user1", group1);
-    user2 = user("user2", group2);
-    user3 = user("user3", group1, group2);
+    user1 = user("user1", "First1 Last1", group1);
+    user2 = user("user2", "First2 Last2", group2);
+    user3 = user("user3", "First3 Last3", group1, group2);
   }
 
   @Test
@@ -116,7 +116,7 @@
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers;
 
-    reviewers = suggestReviewers(changeId, user2.fullName, 2);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name)
         .isEqualTo(user2.fullName);
@@ -126,13 +126,13 @@
     assertThat(reviewers).isEmpty();
 
     setApiUser(user2);
-    reviewers = suggestReviewers(changeId, user2.fullName, 2);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name)
         .isEqualTo(user2.fullName);
 
     setApiUser(user3);
-    reviewers = suggestReviewers(changeId, user2.fullName, 2);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name)
         .isEqualTo(user2.fullName);
@@ -145,13 +145,13 @@
     List<SuggestedReviewerInfo> reviewers;
 
     setApiUser(user1);
-    reviewers = suggestReviewers(changeId, user2.fullName, 2);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).isEmpty();
 
     setApiUser(user1); // Clear cached group info.
     allowGlobalCapabilities(group1.getGroupUUID(),
         GlobalCapability.VIEW_ALL_ACCOUNTS);
-    reviewers = suggestReviewers(changeId, user2.fullName, 2);
+    reviewers = suggestReviewers(changeId, user2.username, 2);
     assertThat(reviewers).hasSize(1);
     assertThat(Iterables.getOnlyElement(reviewers).account.name)
         .isEqualTo(user2.fullName);
@@ -170,9 +170,49 @@
   @GerritConfig(name = "suggest.fullTextSearch", value = "true")
   public void suggestReviewersFullTextSearch() throws Exception {
     String changeId = createChange().getChangeId();
-    List<SuggestedReviewerInfo> reviewers =
-        suggestReviewers(changeId, "ser", 5);
-    assertThat(reviewers).hasSize(4);
+    List<SuggestedReviewerInfo> reviewers;
+
+    reviewers = suggestReviewers(changeId, "first", 4);
+    assertThat(reviewers).hasSize(3);
+
+    reviewers = suggestReviewers(changeId, "first1", 2);
+    assertThat(reviewers).hasSize(1);
+
+    reviewers = suggestReviewers(changeId, "last", 4);
+    assertThat(reviewers).hasSize(3);
+
+    reviewers = suggestReviewers(changeId, "last1", 2);
+    assertThat(reviewers).hasSize(1);
+
+    reviewers = suggestReviewers(changeId, "fi la", 4);
+    assertThat(reviewers).hasSize(3);
+
+    reviewers = suggestReviewers(changeId, "la fi", 4);
+    assertThat(reviewers).hasSize(3);
+
+    reviewers = suggestReviewers(changeId, "first1 la", 2);
+    assertThat(reviewers).hasSize(1);
+
+    reviewers = suggestReviewers(changeId, "fi last1", 2);
+    assertThat(reviewers).hasSize(1);
+
+    reviewers = suggestReviewers(changeId, "first1 last2", 1);
+    assertThat(reviewers).hasSize(0);
+
+    reviewers = suggestReviewers(changeId, name("user"), 7);
+    assertThat(reviewers).hasSize(6);
+
+    reviewers = suggestReviewers(changeId, user1.username, 2);
+    assertThat(reviewers).hasSize(1);
+
+    reviewers = suggestReviewers(changeId, "example.com", 6);
+    assertThat(reviewers).hasSize(5);
+
+    reviewers = suggestReviewers(changeId, user1.email, 2);
+    assertThat(reviewers).hasSize(1);
+
+    reviewers = suggestReviewers(changeId, user1.username + " example", 2);
+    assertThat(reviewers).hasSize(1);
   }
 
   @Test
@@ -183,14 +223,14 @@
   public void suggestReviewersFullTextSearchLimitMaxMatches() throws Exception {
     String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers =
-        suggestReviewers(changeId, "ser", 3);
+        suggestReviewers(changeId, name("user"), 2);
     assertThat(reviewers).hasSize(2);
   }
 
   @Test
   public void suggestReviewersWithoutLimitOptionSpecified() throws Exception {
     String changeId = createChange().getChangeId();
-    String query = user3.fullName;
+    String query = user3.username;
     List<SuggestedReviewerInfo> suggestedReviewerInfos = gApi.changes()
         .id(changeId)
         .suggestReviewers(query)
@@ -214,7 +254,8 @@
     return GroupDescriptions.toAccountGroup(d);
   }
 
-  private TestAccount user(String name, AccountGroup... groups) throws Exception {
+  private TestAccount user(String name, String fullName, AccountGroup... groups)
+      throws Exception {
     name = name(name);
     String[] groupNames = FluentIterable.from(Arrays.asList(groups))
         .transform(new Function<AccountGroup, String>() {
@@ -223,6 +264,6 @@
             return in.getName();
           }
         }).toArray(String.class);
-    return accounts.create(name, name + "@example.com", name, groupNames);
+    return accounts.create(name, name + "@example.com", fullName, groupNames);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index 504f5d7..db6df95 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -75,6 +75,6 @@
     for (AccountGroup.UUID g : state.getOwners()) {
       assertThat(expectedOwners.remove(g)).isTrue();
     }
-    assertThat((Iterable<?>)expectedOwners).isEmpty();
+    assertThat(expectedOwners).isEmpty();
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index c05b56f..721c712 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -15,7 +15,11 @@
 package com.google.gerrit.acceptance.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
+import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -24,6 +28,7 @@
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -36,6 +41,8 @@
 import com.google.gerrit.server.change.Revisions;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.FakeEmailSender;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -64,6 +71,9 @@
   @Inject
   private Provider<PostReview> postReview;
 
+  @Inject
+  private FakeEmailSender email;
+
   private final Integer[] lines = {0, 1};
 
   @Before
@@ -186,6 +196,197 @@
     }
   }
 
+  @Test
+  public void listChangeDrafts() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent",
+          r1.getChangeId())
+        .to("refs/for/master");
+
+
+    setApiUser(admin);
+    addDraft(r1.getChangeId(), r1.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "typo: content"));
+
+    setApiUser(user);
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "+1, please fix"));
+
+    setApiUser(admin);
+    Map<String, List<CommentInfo>> actual =
+        gApi.changes().id(r1.getChangeId()).drafts();
+    assertThat((Iterable<?>) actual.keySet()).containsExactly(FILE_NAME);
+    List<CommentInfo> comments = actual.get(FILE_NAME);
+    assertThat(comments).hasSize(2);
+
+    CommentInfo c1 = comments.get(0);
+    assertThat(c1.author).isNull();
+    assertThat(c1.patchSet).isEqualTo(1);
+    assertThat(c1.message).isEqualTo("nit: trailing whitespace");
+    assertThat(c1.side).isNull();
+    assertThat(c1.line).isEqualTo(1);
+
+    CommentInfo c2 = comments.get(1);
+    assertThat(c2.author).isNull();
+    assertThat(c2.patchSet).isEqualTo(2);
+    assertThat(c2.message).isEqualTo("typo: content");
+    assertThat(c2.side).isNull();
+    assertThat(c2.line).isEqualTo(1);
+  }
+
+  @Test
+  public void listChangeComments() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent",
+          r1.getChangeId())
+        .to("refs/for/master");
+
+    addComment(r1, "nit: trailing whitespace");
+    addComment(r2, "typo: content");
+
+    Map<String, List<CommentInfo>> actual = gApi.changes()
+        .id(r2.getChangeId())
+        .comments();
+    assertThat(actual.keySet()).containsExactly(FILE_NAME);
+
+    List<CommentInfo> comments = actual.get(FILE_NAME);
+    assertThat(comments).hasSize(2);
+
+    CommentInfo c1 = comments.get(0);
+    assertThat(c1.author._accountId).isEqualTo(user.getId().get());
+    assertThat(c1.patchSet).isEqualTo(1);
+    assertThat(c1.message).isEqualTo("nit: trailing whitespace");
+    assertThat(c1.side).isNull();
+    assertThat(c1.line).isEqualTo(1);
+
+    CommentInfo c2 = comments.get(1);
+    assertThat(c2.author._accountId).isEqualTo(user.getId().get());
+    assertThat(c2.patchSet).isEqualTo(2);
+    assertThat(c2.message).isEqualTo("typo: content");
+    assertThat(c2.side).isNull();
+    assertThat(c2.line).isEqualTo(1);
+  }
+
+  @Test
+  public void publishCommentsAllRevisions() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new\ncntent\n",
+          r1.getChangeId())
+        .to("refs/for/master");
+
+    addDraft(r1.getChangeId(), r1.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "nit: trailing whitespace"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "join lines"));
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 2, "typo: content"));
+
+    PushOneCommit.Result other = createChange();
+    // Drafts on other changes aren't returned.
+    addDraft(other.getChangeId(), other.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 1, "unrelated comment"));
+
+    setApiUser(admin);
+    // Drafts by other users aren't returned.
+    addDraft(r2.getChangeId(), r2.getCommit().getName(),
+        newDraft(FILE_NAME, Side.REVISION, 2, "oops"));
+    setApiUser(user);
+
+    ReviewInput reviewInput = new ReviewInput();
+    reviewInput.drafts = DraftHandling.PUBLISH_ALL_REVISIONS;
+    reviewInput.message = "comments";
+    gApi.changes()
+       .id(r2.getChangeId())
+       .current()
+       .review(reviewInput);
+
+    assertThat(gApi.changes()
+          .id(r1.getChangeId())
+          .revision(r1.getCommit().name())
+          .drafts())
+        .isEmpty();
+    Map<String, List<CommentInfo>> ps1Map = gApi.changes()
+        .id(r1.getChangeId())
+        .revision(r1.getCommit().name())
+        .comments();
+    assertThat(ps1Map.keySet()).containsExactly(FILE_NAME);
+    List<CommentInfo> ps1List = ps1Map.get(FILE_NAME);
+    assertThat(ps1List).hasSize(1);
+    assertThat(ps1List.get(0).message).isEqualTo("nit: trailing whitespace");
+
+    assertThat(gApi.changes()
+          .id(r2.getChangeId())
+          .revision(r2.getCommit().name())
+          .drafts())
+        .isEmpty();
+    Map<String, List<CommentInfo>> ps2Map = gApi.changes()
+        .id(r2.getChangeId())
+        .revision(r2.getCommit().name())
+        .comments();
+    assertThat(ps2Map.keySet()).containsExactly(FILE_NAME);
+    List<CommentInfo> ps2List = ps2Map.get(FILE_NAME);
+    assertThat(ps2List).hasSize(2);
+    assertThat(ps2List.get(0).message).isEqualTo("join lines");
+    assertThat(ps2List.get(1).message).isEqualTo("typo: content");
+
+    ImmutableList<Message> messages =
+        email.getMessages(r2.getChangeId(), "comment");
+    assertThat(messages).hasSize(1);
+    String url = canonicalWebUrl.get();
+    int c = r1.getChange().getId().get();
+    assertThat(messages.get(0).body()).contains(
+        "\n"
+        + "Patch Set 2:\n"
+        + "\n"
+        + "(3 comments)\n"
+        + "\n"
+        + "comments\n"
+        + "\n"
+        + url + "#/c/" + c + "/1/a.txt\n"
+        + "File a.txt:\n"
+        + "\n"
+        + "PS1, Line 1: ew\n"
+        + "nit: trailing whitespace\n"
+        + "\n"
+        + "\n"
+        + url + "#/c/" + c + "/2/a.txt\n"
+        + "File a.txt:\n"
+        + "\n"
+        + "PS2, Line 1: ew\n"
+        + "join lines\n"
+        + "\n"
+        + "\n"
+        + "PS2, Line 2: nten\n"
+        + "typo: content\n"
+        + "\n"
+        + "\n"
+        + "-- \n");
+  }
+
+
+  private void addComment(PushOneCommit.Result r, String message)
+      throws Exception {
+    CommentInput c = new CommentInput();
+    c.line = 1;
+    c.message = message;
+    c.path = FILE_NAME;
+    ReviewInput in = new ReviewInput();
+    in.comments = ImmutableMap.<String, List<CommentInput>> of(
+        FILE_NAME, ImmutableList.of(c));
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(in);
+  }
+
   private CommentInfo addDraft(String changeId, String revId, DraftInput in)
       throws Exception {
     return gApi.changes().id(changeId).revision(revId).createDraft(in).get();
@@ -259,9 +460,9 @@
     c.message = message;
     if (line != 0) {
       Comment.Range range = new Comment.Range();
-      range.startLine = 1;
+      range.startLine = line;
       range.startCharacter = 1;
-      range.endLine = 1;
+      range.endLine = line;
       range.endCharacter = 5;
       c.range = range;
     }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index d4268ee..11acf00 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -15,14 +15,15 @@
 package com.google.gerrit.acceptance.server.change;
 
 import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.acceptance.GitUtil.getChangeId;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.change.GetRelated.ChangeAndCommit;
@@ -50,77 +51,65 @@
   @Test
   public void getRelatedNoResult() throws Exception {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
-    PatchSet.Id ps = push.to("refs/for/master").getPatchSetId();
-    List<ChangeAndCommit> related = getRelated(ps);
-    assertThat(related).isEmpty();
+    assertRelated(push.to("refs/for/master").getPatchSetId());
   }
 
   @Test
   public void getRelatedLinear() throws Exception {
-    RevCommit c1 = commitBuilder()
+    RevCommit c1_1 = commitBuilder()
         .add("a.txt", "1")
         .message("subject: 1")
         .create();
-    String id1 = getChangeId(testRepo, c1).get();
-    RevCommit c2 = commitBuilder()
+    String id1 = getChangeId(c1_1);
+    RevCommit c2_2 = commitBuilder()
         .add("b.txt", "2")
         .message("subject: 2")
         .create();
-    String id2 = getChangeId(testRepo, c2).get();
+    String id2 = getChangeId(c2_2);
     pushHead(testRepo, "refs/for/master", false);
 
-    for (RevCommit c : ImmutableList.of(c2, c1)) {
-      List<ChangeAndCommit> related = getRelated(getPatchSetId(c));
-      String id = getChangeId(testRepo, c).get();
-      assertThat(related).hasSize(2);
-      assertThat(related.get(0).changeId)
-          .named("related to " + id).isEqualTo(id2);
-      assertThat(related.get(1).changeId)
-          .named("related to " + id).isEqualTo(id1);
+    for (RevCommit c : ImmutableList.of(c2_2, c1_1)) {
+      assertRelated(getPatchSetId(c),
+          changeAndCommit(id2, c2_2, 1, 1),
+          changeAndCommit(id1, c1_1, 1, 1));
     }
   }
 
   @Test
   public void getRelatedReorder() throws Exception {
     // Create two commits and push.
-    RevCommit c1 = commitBuilder()
+    RevCommit c1_1 = commitBuilder()
         .add("a.txt", "1")
         .message("subject: 1")
         .create();
-    String id1 = getChangeId(testRepo, c1).get();
-    RevCommit c2 = commitBuilder()
+    String id1 = getChangeId(c1_1);
+    RevCommit c2_1 = commitBuilder()
         .add("b.txt", "2")
         .message("subject: 2")
         .create();
-    String id2 = getChangeId(testRepo, c2).get();
+    String id2 = getChangeId(c2_1);
     pushHead(testRepo, "refs/for/master", false);
-    PatchSet.Id c1ps1 = getPatchSetId(c1);
-    PatchSet.Id c2ps1 = getPatchSetId(c2);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
 
     // Swap the order of commits and push again.
     testRepo.reset("HEAD~2");
-    testRepo.cherryPick(c2);
-    testRepo.cherryPick(c1);
+    RevCommit c2_2 = testRepo.cherryPick(c2_1);
+    RevCommit c1_2 = testRepo.cherryPick(c1_1);
     pushHead(testRepo, "refs/for/master", false);
-    PatchSet.Id c1ps2 = getPatchSetId(c1);
-    PatchSet.Id c2ps2 = getPatchSetId(c2);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_1);
 
-    for (PatchSet.Id ps : ImmutableList.of(c2ps2, c1ps2)) {
-      List<ChangeAndCommit> related = getRelated(ps);
-      assertThat(related).hasSize(2);
-      assertThat(related.get(0).changeId).named("related to " + ps)
-          .isEqualTo(id1);
-      assertThat(related.get(1).changeId).named("related to " + ps)
-          .isEqualTo(id2);
+    for (PatchSet.Id ps : ImmutableList.of(ps2_2, ps1_2)) {
+      assertRelated(ps,
+          changeAndCommit(id1, c1_2, 2, 2),
+          changeAndCommit(id2, c2_2, 2, 2));
     }
 
-    for (PatchSet.Id ps : ImmutableList.of(c2ps1, c1ps1)) {
-      List<ChangeAndCommit> related = getRelated(ps);
-      assertThat(related).hasSize(2);
-      assertThat(related.get(0).changeId).named("related to " + ps)
-          .isEqualTo(id2);
-      assertThat(related.get(1).changeId).named("related to " + ps)
-          .isEqualTo(id1);
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(id2, c2_1, 1, 2),
+          changeAndCommit(id1, c1_1, 1, 2));
     }
   }
 
@@ -128,95 +117,91 @@
   public void getRelatedReorderAndExtend() throws Exception {
     // Create two commits and push.
     ObjectId initial = repo().getRef("HEAD").getObjectId();
-    RevCommit c1 = commitBuilder()
+    RevCommit c1_1 = commitBuilder()
         .add("a.txt", "1")
         .message("subject: 1")
         .create();
-    String id1 = getChangeId(testRepo, c1).get();
-    RevCommit c2 = commitBuilder()
+    String id1 = getChangeId(c1_1);
+    RevCommit c2_1 = commitBuilder()
         .add("b.txt", "2")
         .message("subject: 2")
         .create();
-    String id2 = getChangeId(testRepo, c2).get();
+    String id2 = getChangeId(c2_1);
     pushHead(testRepo, "refs/for/master", false);
-    PatchSet.Id c1ps1 = getPatchSetId(c1);
-    PatchSet.Id c2ps1 = getPatchSetId(c2);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
 
     // Swap the order of commits, create a new commit on top, and push again.
     testRepo.reset(initial);
-    testRepo.cherryPick(c2);
-    testRepo.cherryPick(c1);
-    RevCommit c3 = commitBuilder()
+    RevCommit c2_2 = testRepo.cherryPick(c2_1);
+    RevCommit c1_2 = testRepo.cherryPick(c1_1);
+    RevCommit c3_1 = commitBuilder()
         .add("c.txt", "3")
         .message("subject: 3")
         .create();
-    String id3 = getChangeId(testRepo, c3).get();
+    String id3 = getChangeId(c3_1);
     pushHead(testRepo, "refs/for/master", false);
-    PatchSet.Id c1ps2 = getPatchSetId(c1);
-    PatchSet.Id c2ps2 = getPatchSetId(c2);
-    PatchSet.Id c3ps1 = getPatchSetId(c3);
+    PatchSet.Id ps1_2 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_2 = getPatchSetId(c2_1);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
 
-
-    for (PatchSet.Id ps : ImmutableList.of(c3ps1, c2ps2, c1ps2)) {
-      List<ChangeAndCommit> related = getRelated(ps);
-      assertThat(related).hasSize(3);
-      assertThat(related.get(0).changeId).named("related to " + ps)
-          .isEqualTo(id3);
-      assertThat(related.get(1).changeId).named("related to " + ps)
-          .isEqualTo(id1);
-      assertThat(related.get(2).changeId).named("related to " + ps)
-          .isEqualTo(id2);
+    for (PatchSet.Id ps : ImmutableList.of(ps3_1, ps2_2, ps1_2)) {
+      assertRelated(ps,
+          changeAndCommit(id3, c3_1, 1, 1),
+          changeAndCommit(id1, c1_2, 2, 2),
+          changeAndCommit(id2, c2_2, 2, 2));
     }
 
-    for (PatchSet.Id ps : ImmutableList.of(c2ps1, c1ps1)) {
-      List<ChangeAndCommit> related = getRelated(ps);
-      assertThat(related).hasSize(3);
-      assertThat(related.get(0).changeId).named("related to " + ps)
-          .isEqualTo(id3);
-      assertThat(related.get(1).changeId).named("related to " + ps)
-          .isEqualTo(id2);
-      assertThat(related.get(2).changeId).named("related to " + ps)
-          .isEqualTo(id1);
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps1_1)) {
+      assertRelated(ps,
+          changeAndCommit(id3, c3_1, 1, 1),
+          changeAndCommit(id2, c2_1, 1, 2),
+          changeAndCommit(id1, c1_1, 1, 2));
     }
   }
 
   @Test
   public void getRelatedEdit() throws Exception {
-    RevCommit c1 = commitBuilder()
+    RevCommit c1_1 = commitBuilder()
         .add("a.txt", "1")
         .message("subject: 1")
         .create();
-    String id1 = getChangeId(testRepo, c1).get();
-    RevCommit c2 = commitBuilder()
+    String id1 = getChangeId(c1_1);
+    RevCommit c2_1 = commitBuilder()
         .add("b.txt", "2")
         .message("subject: 2")
         .create();
-    String id2 = getChangeId(testRepo, c2).get();
-    RevCommit c3 = commitBuilder()
+    String id2 = getChangeId(c2_1);
+    RevCommit c3_1 = commitBuilder()
         .add("c.txt", "3")
         .message("subject: 3")
         .create();
-    String id3 = getChangeId(testRepo, c3).get();
+    String id3 = getChangeId(c3_1);
     pushHead(testRepo, "refs/for/master", false);
 
-    Change ch2 = getChange(c2).change();
+    Change ch2 = getChange(c2_1).change();
     editModifier.createEdit(ch2, getPatchSet(ch2));
     editModifier.modifyFile(editUtil.byChange(ch2).get(), "a.txt",
         RestSession.newRawInput(new byte[] {'a'}));
-    String editRev = editUtil.byChange(ch2).get().getRevision().get();
+    ObjectId editRev =
+        ObjectId.fromString(editUtil.byChange(ch2).get().getRevision().get());
 
-    List<ChangeAndCommit> related = getRelated(ch2.getId(), 0);
-    assertThat(related).hasSize(3);
-    assertThat(related.get(0).changeId).named("related to " + id2)
-        .isEqualTo(id3);
-    assertThat(related.get(1).changeId).named("related to " + id2)
-        .isEqualTo(id2);
-    assertThat(related.get(1)._revisionNumber).named(
-        "has edit revision number").isEqualTo(0);
-    assertThat(related.get(1).commit.commit).named(
-        "has edit revision " + editRev).isEqualTo(editRev);
-    assertThat(related.get(2).changeId).named("related to " + id2)
-        .isEqualTo(id1);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+    PatchSet.Id ps2_edit = new PatchSet.Id(ch2.getId(), 0);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    for (PatchSet.Id ps : ImmutableList.of(ps1_1, ps2_1, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(id3, c3_1, 1, 1),
+          changeAndCommit(id2, c2_1, 1, 1),
+          changeAndCommit(id1, c1_1, 1, 1));
+    }
+
+    assertRelated(ps2_edit,
+        changeAndCommit(id3, c3_1, 1, 1),
+        changeAndCommit(id2, editRev, 0, 1),
+        changeAndCommit(id1, c1_1, 1, 1));
   }
 
   private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws IOException {
@@ -231,6 +216,10 @@
         RelatedInfo.class).changes;
   }
 
+  private String getChangeId(RevCommit c) throws Exception {
+    return GitUtil.getChangeId(testRepo, c).get();
+  }
+
   private PatchSet.Id getPatchSetId(ObjectId c) throws OrmException {
     return getChange(c).change().currentPatchSetId();
   }
@@ -242,4 +231,35 @@
   private ChangeData getChange(ObjectId c) throws OrmException {
     return Iterables.getOnlyElement(queryProvider.get().byCommit(c));
   }
+
+  private static ChangeAndCommit changeAndCommit(String changeId,
+      ObjectId commitId, int revisionNum, int currentRevisionNum) {
+    ChangeAndCommit result = new ChangeAndCommit();
+    result.changeId = changeId;
+    result.commit = new CommitInfo();
+    result.commit.commit = commitId.name();
+    result._revisionNumber = revisionNum;
+    result._currentRevisionNumber = currentRevisionNum;
+    return result;
+  }
+
+  private void assertRelated(PatchSet.Id psId, ChangeAndCommit... expected)
+      throws Exception {
+    List<ChangeAndCommit> actual = getRelated(psId);
+    assertThat(actual).hasSize(expected.length);
+    for (int i = 0; i < actual.size(); i++) {
+      String name = "index " + i + " related to " + psId;
+      ChangeAndCommit a = actual.get(i);
+      ChangeAndCommit e = expected[i];
+      assertThat(a.changeId).named("Change-Id of " + name)
+          .isEqualTo(e.changeId);
+      assertThat(a.commit.commit).named("commit of " + name)
+          .isEqualTo(e.commit.commit);
+      // Don't bother checking _changeNumber; assume changeId is sufficient.
+      assertThat(a._revisionNumber).named("revision of " + name)
+          .isEqualTo(e._revisionNumber);
+      assertThat(a._currentRevisionNumber).named("current revision of " + name)
+          .isEqualTo(e._currentRevisionNumber);
+    }
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
index 2ea5dec..d067b34 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/BUCK
@@ -2,5 +2,6 @@
 
 acceptance_tests(
   srcs = glob(['*IT.java']),
+  deps = ['//lib/commons:compress'],
   labels = ['ssh'],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index 91e9403..4f9f190 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -93,7 +93,7 @@
     GarbageCollectionResult result = garbageCollectionFactory.create().run(
         Arrays.asList(allProjects, project, project2, project3));
     assertThat(result.hasErrors()).isTrue();
-    assertThat(result.getErrors().size()).isEqualTo(1);
+    assertThat(result.getErrors()).hasSize(1);
     GarbageCollectionResult.Error error = result.getErrors().get(0);
     assertThat(error.getType()).isEqualTo(
         GarbageCollectionResult.Error.Type.GC_ALREADY_SCHEDULED);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
index ae0c8c1..7fb99b6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/JschVerifyFalseBugIT.java
@@ -62,6 +62,6 @@
     for (Future<Void> future : futures) {
       future.get();
     }
-    assertThat(futures.size()).isEqualTo(threads);
+    assertThat(futures).hasSize(threads);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
new file mode 100644
index 0000000..88821ce
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
+import org.eclipse.jgit.transport.PacketLineIn;
+import org.eclipse.jgit.transport.PacketLineOut;
+import org.eclipse.jgit.util.IO;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+import java.util.TreeSet;
+
+@NoHttpd
+public class UploadArchiveIT extends AbstractDaemonTest {
+
+  @Test
+  @GerritConfig(name = "download.archive", value = "off")
+  public void archiveFeatureOff() throws Exception {
+    archiveNotPermitted();
+  }
+
+  @Test
+  @GerritConfig(name = "download.archive", values = {"tar", "tbz2", "tgz", "txz"})
+  public void zipFormatDisabled() throws Exception {
+    archiveNotPermitted();
+  }
+
+  @Test
+  public void zipFormat() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String abbreviated = r.getCommitId().abbreviate(8).name();
+    String c = command(r, abbreviated);
+
+    InputStream out =
+        sshSession.exec2("git-upload-archive " + project.get(),
+            argumentsToInputStream(c));
+
+    // Wrap with PacketLineIn to read ACK bytes from output stream
+    PacketLineIn in = new PacketLineIn(out);
+    String tmp = in.readString();
+    assertThat(tmp).isEqualTo("ACK");
+    tmp = in.readString();
+
+    // Skip length (4 bytes) + 1 byte
+    // to position the output stream to the raw zip stream
+    byte[] buffer = new byte[5];
+    IO.readFully(out, buffer, 0, 5);
+    Set<String> entryNames = new TreeSet<>();
+    try (ZipArchiveInputStream zip = new ZipArchiveInputStream(out)) {
+      ZipArchiveEntry zipEntry = zip.getNextZipEntry();
+      while (zipEntry != null) {
+        String name = zipEntry.getName();
+        entryNames.add(name);
+        zipEntry = zip.getNextZipEntry();
+      }
+    }
+
+    assertThat(entryNames.size()).isEqualTo(1);
+    assertThat(Iterables.getOnlyElement(entryNames)).isEqualTo(
+        String.format("%s/%s", abbreviated, PushOneCommit.FILE_NAME));
+  }
+
+  private String command(PushOneCommit.Result r, String abbreviated) {
+    String c = "-f=zip "
+        + "-9 "
+        + "--prefix=" + abbreviated + "/ "
+        + r.getCommit().name() + " "
+        + PushOneCommit.FILE_NAME;
+    return c;
+  }
+
+  private void archiveNotPermitted() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String abbreviated = r.getCommitId().abbreviate(8).name();
+    String c = command(r, abbreviated);
+
+    InputStream out =
+        sshSession.exec2("git-upload-archive " + project.get(),
+            argumentsToInputStream(c));
+
+    // Wrap with PacketLineIn to read ACK bytes from output stream
+    PacketLineIn in = new PacketLineIn(out);
+    String tmp = in.readString();
+    assertThat(tmp).isEqualTo("ACK");
+    tmp = in.readString();
+    tmp = in.readString();
+    tmp = tmp.substring(1);
+    assertThat(tmp).isEqualTo("fatal: upload-archive not permitted");
+  }
+
+  private InputStream argumentsToInputStream(String c) throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    PacketLineOut pctOut = new PacketLineOut(out);
+    for (String arg : Splitter.on(' ').split(c)) {
+      pctOut.writeString("argument " + arg);
+    }
+    pctOut.end();
+    return new ByteArrayInputStream(out.toByteArray());
+  }
+}
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 1aaebf5..123bb9a 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -388,7 +388,7 @@
           return b;
         }
       } catch (SQLException e) {
-        log.warn("Cannot build BloomFilter for " + url, e);
+        log.warn("Cannot build BloomFilter for " + url + ": " + e.getMessage());
         c = close(c);
         return null;
       } finally {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
index d911390..045591c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
@@ -16,10 +16,7 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.reviewdb.client.Project;
 
 import java.util.List;
 import java.util.Set;
@@ -36,17 +33,11 @@
   protected boolean httpPasswordSettingsEnabled = true;
 
   protected GitwebConfig gitweb;
-  protected boolean useContributorAgreements;
-  protected boolean useContactInfo;
-  protected boolean allowRegisterNewEmail;
   protected AuthType authType;
-  protected Set<DownloadScheme> downloadSchemes;
-  protected Set<DownloadCommand> downloadCommands;
   protected String gitDaemonUrl;
   protected String gitHttpUrl;
   protected String sshdAddress;
   protected String editFullNameUrl;
-  protected Project.NameKey wildProject;
   protected Set<Account.FieldName> editableAccountFields;
   protected boolean documentationAvailable;
   protected String anonymousCowardName;
@@ -138,30 +129,10 @@
     httpPasswordUrl = url;
   }
 
-  public AuthType getAuthType() {
-    return authType;
-  }
-
   public void setAuthType(final AuthType t) {
     authType = t;
   }
 
-  public Set<DownloadScheme> getDownloadSchemes() {
-    return downloadSchemes;
-  }
-
-  public void setDownloadSchemes(final Set<DownloadScheme> s) {
-    downloadSchemes = s;
-  }
-
-  public Set<DownloadCommand> getDownloadCommands() {
-    return downloadCommands;
-  }
-
-  public void setDownloadCommands(final Set<DownloadCommand> downloadCommands) {
-    this.downloadCommands = downloadCommands;
-  }
-
   public GitwebConfig getGitwebLink() {
     return gitweb;
   }
@@ -170,22 +141,6 @@
     gitweb = w;
   }
 
-  public boolean isUseContributorAgreements() {
-    return useContributorAgreements;
-  }
-
-  public void setUseContributorAgreements(final boolean r) {
-    useContributorAgreements = r;
-  }
-
-  public boolean isUseContactInfo() {
-    return useContactInfo;
-  }
-
-  public void setUseContactInfo(final boolean r) {
-    useContactInfo = r;
-  }
-
   public String getGitDaemonUrl() {
     return gitDaemonUrl;
   }
@@ -216,22 +171,6 @@
     sshdAddress = addr;
   }
 
-  public Project.NameKey getWildProject() {
-    return wildProject;
-  }
-
-  public void setWildProject(final Project.NameKey wp) {
-    wildProject = wp;
-  }
-
-  public boolean canEdit(final Account.FieldName f) {
-    return editableAccountFields.contains(f);
-  }
-
-  public Set<Account.FieldName> getEditableAccountFields() {
-    return editableAccountFields;
-  }
-
   public void setEditableAccountFields(final Set<Account.FieldName> af) {
     editableAccountFields = af;
   }
@@ -261,9 +200,9 @@
   }
 
   public boolean siteHasUsernames() {
-    if (getAuthType() == AuthType.CUSTOM_EXTENSION
+    if (authType == AuthType.CUSTOM_EXTENSION
         && getHttpPasswordUrl() != null
-        && !canEdit(FieldName.USER_NAME)) {
+        && !editableAccountFields.contains(FieldName.USER_NAME)) {
       return false;
     }
     return true;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 046df1d..7bab43a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -57,6 +57,8 @@
   protected boolean intralineFailure;
   protected boolean intralineTimeout;
   protected boolean binary;
+  protected transient String commitIdA;
+  protected transient String commitIdB;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
@@ -65,7 +67,8 @@
       final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
       final String mta, final String mtb, final CommentDetail cd,
       final List<Patch> hist, final boolean hf, final boolean id,
-      final boolean idf, final boolean idt, boolean bin) {
+      final boolean idf, final boolean idt, boolean bin,
+      final String cma, final String cmb) {
     changeId = ck;
     changeType = ct;
     oldName = on;
@@ -88,6 +91,8 @@
     intralineFailure = idf;
     intralineTimeout = idt;
     binary = bin;
+    commitIdA = cma;
+    commitIdB = cmb;
   }
 
   protected PatchScript() {
@@ -200,4 +205,12 @@
   public boolean isBinary() {
     return binary;
   }
+
+  public String getCommitIdA() {
+    return commitIdA;
+  }
+
+  public String getCommitIdB() {
+    return commitIdB;
+  }
 }
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 06f0a75..6781af2 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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -23,6 +24,7 @@
 
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public interface ChangeApi {
@@ -106,6 +108,24 @@
    */
   Set<String> getHashtags() throws RestApiException;
 
+  /**
+   * Get all published comments on a change.
+   *
+   * @return comments in a map keyed by path; comments have the {@code revision}
+   *     field set to indicate their patch set.
+   * @throws RestApiException
+   */
+  Map<String, List<CommentInfo>> comments() throws RestApiException;
+
+  /**
+   * Get all draft comments for the current user on a change.
+   *
+   * @return drafts in a map keyed by path; comments have the {@code revision}
+   *     field set to indicate their patch set.
+   * @throws RestApiException
+   */
+  Map<String, List<CommentInfo>> drafts() throws RestApiException;
+
   ChangeInfo check() throws RestApiException;
   ChangeInfo check(FixInput fix) throws RestApiException;
 
@@ -250,6 +270,16 @@
     }
 
     @Override
+    public Map<String, List<CommentInfo>> comments() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public Map<String, List<CommentInfo>> drafts() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public ChangeInfo check() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index dd2ce92..2d2e4e9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -61,7 +61,17 @@
   public String onBehalfOf;
 
   public static enum DraftHandling {
-    DELETE, PUBLISH, KEEP
+    /** Delete pending drafts on this revision only. */
+    DELETE,
+
+    /** Publish pending drafts on this revision only. */
+    PUBLISH,
+
+    /** Leave pending drafts alone. */
+    KEEP,
+
+    /** Publish pending drafts on all revisions. */
+    PUBLISH_ALL_REVISIONS
   }
 
   public static enum NotifyHandling {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 3e7a207..3d1e3bd 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -50,6 +50,9 @@
   Map<String, List<CommentInfo>> comments() throws RestApiException;
   Map<String, List<CommentInfo>> drafts() throws RestApiException;
 
+  List<CommentInfo> commentsAsList() throws RestApiException;
+  List<CommentInfo> draftsAsList() throws RestApiException;
+
   DraftApi createDraft(DraftInput in) throws RestApiException;
   DraftApi draft(String id) throws RestApiException;
 
@@ -148,6 +151,16 @@
     }
 
     @Override
+    public List<CommentInfo> commentsAsList() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
+    public List<CommentInfo> draftsAsList() throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public Map<String, List<CommentInfo>> drafts() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
index 7a626f1..0e848b9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -62,7 +62,7 @@
 
   public abstract class ListRequest {
     public static enum FilterType {
-      CODE, PARENT_CANDIDATES, PERMISSIONS, ALL;
+      CODE, PARENT_CANDIDATES, PERMISSIONS, ALL
     }
 
     private final List<String> branches = new ArrayList<>();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
index e79df1c..b9863d7 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
@@ -17,6 +17,13 @@
 import java.sql.Timestamp;
 
 public abstract class Comment {
+  /**
+   * Patch set number containing this commit.
+   * <p>
+   * Only set in contexts where comments may come from multiple patch sets.
+   */
+  public Integer patchSet;
+
   public String id;
   public String path;
   public Side side;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
index 54617a7..5caa903 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.client;
 
 import java.util.EnumSet;
+import java.util.Set;
 
 /** Output options available for retrieval change details. */
 public enum ListChangesOption {
@@ -58,7 +59,10 @@
   CHECK(15),
 
   /** Include allowed change actions client could perform. */
-  CHANGE_ACTIONS(16);
+  CHANGE_ACTIONS(16),
+
+  /** Include a copy of commit messages including review footers. */
+  COMMIT_FOOTERS(17);
 
   private final int value;
 
@@ -87,7 +91,7 @@
     return r;
   }
 
-  public static int toBits(EnumSet<ListChangesOption> set) {
+  public static int toBits(Set<ListChangesOption> set) {
     int r = 0;
     for (ListChangesOption o : set) {
       r |= 1 << o.value;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java
index d55580c..d26ea23 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeType.java
@@ -32,5 +32,5 @@
   COPIED,
 
   /** Sufficient amount of content changed to claim the file was rewritten. */
-  REWRITE;
+  REWRITE
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
index 62b1dc7..58b2d39 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
@@ -42,6 +42,8 @@
   }
 
   public static class FileMeta {
+    // The ID of the commit containing the file
+    public transient String commitId;
     // The name of the file
     public String name;
     // The content type of the file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
index a117d07..d04b346 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ProblemInfo.java
@@ -16,7 +16,7 @@
 
 public class ProblemInfo {
   public static enum Status {
-    FIXED, FIX_FAILED;
+    FIXED, FIX_FAILED
   }
 
   public String message;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
index 4b8eec1..7c71ba3 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -29,4 +29,5 @@
   public CommitInfo commit;
   public Map<String, FileInfo> files;
   public Map<String, ActionInfo> actions;
+  public String commitWithFooters;
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
new file mode 100644
index 0000000..eb223c6
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GarbageCollectorListener.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.util.Properties;
+
+/**
+ * Notified whenever the garbage collector has run successfully on a project.
+ */
+@ExtensionPoint
+public interface GarbageCollectorListener {
+  public interface Event {
+    /** @return The name of the project that has been garbage collected. */
+    String getProjectName();
+
+    /**
+     * Properties describing the result of the garbage collection performed by
+     * JGit
+     *
+     * @see <a href="http://download.eclipse.org/jgit/site/3.7.0.201502260915-r/apidocs/org/eclipse/jgit/api/GarbageCollectCommand.html#call%28%29">GarbageCollectCommand</a>
+     */
+    Properties getStatistics();
+  }
+
+  void onGarbageCollected(Event event);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
index 053999a..7f17f4f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -72,7 +72,7 @@
     } else if (isGerritServer(account)) {
       setVisible(true);
       setResource(Gerrit.RESOURCES.gerritAvatar26());
-    } else if (account.has_avatar_info()) {
+    } else if (account.hasAvatarInfo()) {
       setVisible(false);
       AvatarInfo info = account.avatar(size);
       if (info != null) {
@@ -121,7 +121,7 @@
   }
 
   private static boolean isGerritServer(AccountInfo account) {
-    return account._account_id() == 0
+    return account._accountId() == 0
         && Util.C.messageNoAuthor().equals(account.name());
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 000f9e6..e43139c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -695,7 +695,7 @@
         }
 
         if (matchExact(SETTINGS_AGREEMENTS, token)
-            && Gerrit.getConfig().isUseContributorAgreements()) {
+            && Gerrit.info().auth().useContributorAgreements()) {
           return new MyAgreementsScreen();
         }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index 5b51b48..30980e1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -144,8 +144,8 @@
     StringBuilder b = new StringBuilder().append(name);
     if (info.email() != null) {
       b.append(" <").append(info.email()).append(">");
-    } else if (info._account_id() > 0) {
-      b.append(" (").append(info._account_id()).append(")");
+    } else if (info._accountId() > 0) {
+      b.append(" (").append(info._accountId()).append(")");
     }
     return b.toString();
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 65e2858..c176a09 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.client.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
 import com.google.gerrit.client.config.ConfigServerApi;
+import com.google.gerrit.client.config.ServerInfo;
 import com.google.gerrit.client.extensions.TopMenu;
 import com.google.gerrit.client.extensions.TopMenuItem;
 import com.google.gerrit.client.extensions.TopMenuList;
@@ -49,7 +50,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.EntryPoint;
@@ -106,6 +106,7 @@
 
   private static String myHost;
   private static GerritConfig myConfig;
+  private static ServerInfo myServerInfo;
   private static HostPageData.Theme myTheme;
   private static Account myAccount;
   private static String defaultScreenToken;
@@ -288,6 +289,11 @@
     return myConfig;
   }
 
+  /** Get the public configuration data used by this Gerrit instance. */
+  public static ServerInfo info() {
+    return myServerInfo;
+  }
+
   public static GitwebLink getGitwebLink() {
     GitwebConfig gw = getConfig().getGitwebLink();
     return gw != null && gw.type != null ? new GitwebLink(gw) : null;
@@ -426,8 +432,16 @@
     initHostname();
     Window.setTitle(M.windowTitle1(myHost));
 
-    final HostPageDataService hpd = GWT.create(HostPageDataService.class);
-    hpd.load(new GerritCallback<HostPageData>() {
+    RpcStatus.INSTANCE = new RpcStatus();
+    CallbackGroup cbg = new CallbackGroup();
+    ConfigServerApi.serverInfo(cbg.add(new GerritCallback<ServerInfo>() {
+      @Override
+      public void onSuccess(ServerInfo info) {
+        myServerInfo = info;
+      }
+    }));
+    HostPageDataService hpd = GWT.create(HostPageDataService.class);
+    hpd.load(cbg.addFinal(new GerritCallback<HostPageData>() {
       @Override
       public void onSuccess(final HostPageData result) {
         Document.get().getElementById("gerrit_hostpagedata").removeFromParent();
@@ -444,7 +458,7 @@
         }
         onModuleLoad2(result);
       }
-    });
+    }));
   }
 
   private static void initHostname() {
@@ -538,7 +552,6 @@
     };
     gBody.add(body);
 
-    RpcStatus.INSTANCE = new RpcStatus();
     JsonUtil.addRpcStartHandler(RpcStatus.INSTANCE);
     JsonUtil.addRpcCompleteHandler(RpcStatus.INSTANCE);
     JsonUtil.setDefaultXsrfManager(new XsrfManager() {
@@ -705,9 +718,9 @@
     }
 
     if (signedIn) {
-      whoAmI(cfg.getAuthType() != AuthType.CLIENT_SSL_CERT_LDAP);
+      whoAmI(!info().auth().isClientSslCertLdap());
     } else {
-      switch (cfg.getAuthType()) {
+      switch (info().auth().authType()) {
         case CLIENT_SSL_CERT_LDAP:
           break;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml
index 5f7bebf..36d08b1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/MessageOfTheDayBar.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .popup {
       position: fixed;
       top: 5px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 90348db..c8bb731 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.AnchorElement;
 import com.google.gwt.dom.client.Element;
@@ -53,8 +52,8 @@
     if (showSettingsLink) {
       if (Gerrit.getConfig().getSwitchAccountUrl() != null) {
         switchAccount.setHref(Gerrit.getConfig().getSwitchAccountUrl());
-      } else if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT
-          || Gerrit.getConfig().getAuthType() == AuthType.OPENID) {
+      } else if (Gerrit.info().auth().isDev()
+          || Gerrit.info().auth().isOpenId()) {
         switchAccount.setHref(Gerrit.selfRedirect("/login/"));
       } else {
         switchAccount.removeFromParent();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
index ec50b30..8f5073d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.ui.xml
@@ -19,7 +19,7 @@
   xmlns:g='urn:import:com.google.gwt.user.client.ui'
   xmlns:gerrit='urn:import:com.google.gerrit.client'
   xmlns:u='urn:import:com.google.gerrit.client.ui'>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .panel {
       padding: 8px;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
index 1127374..36fa98d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
@@ -18,7 +18,7 @@
 import com.google.gwt.core.client.JsArray;
 
 public class AccountInfo extends JavaScriptObject {
-  public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
+  public final native int _accountId() /*-{ return this._account_id || 0; }-*/;
   public final native String name() /*-{ return this.name; }-*/;
   public final native String email() /*-{ return this.email; }-*/;
   public final native String username() /*-{ return this.username; }-*/;
@@ -29,7 +29,7 @@
    *         available, such as when no plugin is installed. This method returns
    *         false if the server did not check on avatars for the account.
    */
-  public final native boolean has_avatar_info()
+  public final native boolean hasAvatarInfo()
   /*-{ return this.hasOwnProperty('avatars') }-*/;
 
   public final AvatarInfo avatar(int sz) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
index 4b8f0e2..33d11a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
@@ -65,7 +65,7 @@
     hasContact.setStyleName(Gerrit.RESOURCES.css().accountContactOnFile());
     hasContact.setVisible(false);
 
-    if (Gerrit.getConfig().isUseContactInfo()) {
+    if (Gerrit.info().hasContactStore()) {
       body.add(privhtml);
       body.add(hasContact);
       body.add(infoSecure);
@@ -116,7 +116,7 @@
   @Override
   ContactInformation toContactInformation() {
     final ContactInformation info;
-    if (Gerrit.getConfig().isUseContactInfo()) {
+    if (Gerrit.info().hasContactStore()) {
       info = new ContactInformation();
       info.setAddress(addressTxt.getText());
       info.setCountry(countryTxt.getText());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index 405adcf..2d9aa73 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ChangeEvent;
@@ -101,7 +100,7 @@
     }
 
     int row = 0;
-    if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)
+    if (!Gerrit.info().auth().canEdit(FieldName.USER_NAME)
         && Gerrit.getConfig().siteHasUsernames()) {
       infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
       row(infoPlainText, row++, Util.C.userName(), new UsernameField());
@@ -168,11 +167,11 @@
   }
 
   private boolean canEditFullName() {
-    return Gerrit.getConfig().canEdit(Account.FieldName.FULL_NAME);
+    return Gerrit.info().auth().canEdit(Account.FieldName.FULL_NAME);
   }
 
   private boolean canRegisterNewEmail() {
-    return Gerrit.getConfig().canEdit(Account.FieldName.REGISTER_NEW_EMAIL);
+    return Gerrit.info().auth().canEdit(Account.FieldName.REGISTER_NEW_EMAIL);
   }
 
   void hideSaveButton() {
@@ -275,7 +274,7 @@
           @Override
           public void onSuccess(EmailInfo result) {
             box.hide();
-            if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+            if (Gerrit.info().auth().isDev()) {
               currentEmail = addr;
               if (emailPick.getItemCount() == 0) {
                 final Account me = Gerrit.getUserAccount();
@@ -325,7 +324,7 @@
     buttons.add(register);
     buttons.add(cancel);
 
-    if (Gerrit.getConfig().getAuthType() != AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+    if (!Gerrit.info().auth().isDev()) {
       body.add(new HTML(Util.C.descRegisterNewEmail()));
     }
     body.add(inEmail);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
index af70fac..b638575 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
@@ -59,7 +58,8 @@
     });
     add(deleteIdentity);
 
-    if (Gerrit.getConfig().getAuthType() == AuthType.OPENID) {
+    if (Gerrit.info().auth().isOpenId()
+        || Gerrit.info().auth().isOAuth()) {
       Button linkIdentity = new Button(Util.C.buttonLinkIdentity());
       linkIdentity.addClickHandler(new ClickHandler() {
         @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
index 2810931..5f1e383 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
@@ -70,7 +70,7 @@
     formBody.add(contactGroup);
 
     if (Gerrit.getUserAccount().getUserName() == null
-        && Gerrit.getConfig().canEdit(FieldName.USER_NAME)) {
+        && Gerrit.info().auth().canEdit(FieldName.USER_NAME)) {
       final FlowPanel fp = new FlowPanel();
       fp.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
       fp.add(new SmallHeading(Util.C.welcomeUsernameHeading()));
@@ -116,7 +116,7 @@
 
     final FlowPanel choices = new FlowPanel();
     choices.setStyleName(Gerrit.RESOURCES.css().registerScreenNextLinks());
-    if (Gerrit.getConfig().isUseContributorAgreements()) {
+    if (Gerrit.info().auth().useContributorAgreements()) {
       final FlowPanel agreementGroup = new FlowPanel();
       agreementGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
       agreementGroup.add(new SmallHeading(Util.C.welcomeAgreementHeading()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
index c689b49..ca4ac20 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
@@ -34,7 +34,7 @@
     }
     link(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
     link(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
-    if (Gerrit.getConfig().isUseContributorAgreements()) {
+    if (Gerrit.info().auth().useContributorAgreements()) {
       link(Util.C.tabAgreements(), PageLinks.SETTINGS_AGREEMENTS);
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
index 2077d6b..e440d55 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -86,7 +86,7 @@
   }
 
   private boolean canEditUserName() {
-    return Gerrit.getConfig().canEdit(Account.FieldName.USER_NAME);
+    return Gerrit.info().auth().canEdit(Account.FieldName.USER_NAME);
   }
 
   private void confirmSetUserName() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index 157748f..aa72300 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -250,8 +250,7 @@
     if (value.getPermission(permissionName) != null) {
       return;
     }
-    if (Gerrit.getConfig().getWildProject()
-        .equals(projectAccess.getProjectName())
+    if (Gerrit.info().gerrit().isAllProjects(projectAccess.getProjectName())
         && !Permission.canBeOnAllProjects(value.getName(), permissionName)) {
       return;
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
index cc80712..52f3588 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
@@ -24,7 +24,7 @@
   ui:generateLocales='default,en'
   >
 <ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
-<ui:style gss="false">
+<ui:style gss='false'>
   @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
   @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index d3f659e..1e3f918 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -248,7 +248,7 @@
       for (int row = 1; row < table.getRowCount(); row++) {
         final AccountInfo i = getRowItem(row);
         if (i != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          ids.add(i._account_id());
+          ids.add(i._accountId());
         }
       }
       if (!ids.isEmpty()) {
@@ -258,7 +258,7 @@
               public void onSuccess(final VoidResult result) {
                 for (int row = 1; row < table.getRowCount();) {
                   final AccountInfo i = getRowItem(row);
-                  if (i != null && ids.contains(i._account_id())) {
+                  if (i != null && ids.contains(i._accountId())) {
                     table.removeRow(row);
                   } else {
                     row++;
@@ -296,7 +296,7 @@
             return cmp;
           }
 
-          return a._account_id() - b._account_id();
+          return a._accountId() - b._accountId();
         }
 
         public String nullToEmpty(String str) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
index 1ffd6f0..9092508 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
@@ -44,7 +44,7 @@
             public void onSuccess(ChangeInfo result) {
               sent = true;
               hide();
-              Gerrit.display(PageLinks.toChange(result.legacy_id()));
+              Gerrit.display(PageLinks.toChange(result.legacyId()));
             }
 
             @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
index 86a31ee..0fac957 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
@@ -32,7 +32,7 @@
           @Override
           public void onSuccess(ChangeInfo result) {
             Gerrit.display(Dispatcher.toEditScreen(
-                new PatchSet.Id(result.legacy_id(), 1), "project.config"));
+                new PatchSet.Id(result.legacyId(), 1), "project.config"));
           }
 
           @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
index f9b445b..00c41dc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
@@ -24,7 +24,7 @@
   ui:generateLocales='default,en'
   >
 <ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
-<ui:style gss="false">
+<ui:style gss='false'>
   @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
   @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
index 4150eb5..644fef4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
@@ -25,7 +25,7 @@
   ui:generateLocales='default,en'
   >
 <ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
-<ui:style gss="false">
+<ui:style gss='false'>
   @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
 
   .panel {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
index f319637..0db4779 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
@@ -22,7 +22,7 @@
   ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
   ui:generateLocales='default,en'
   >
-<ui:style gss="false">
+<ui:style gss='false'>
   .inheritsFrom {
     margin-bottom: 0.5em;
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
index 519d0e2..724c7a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
@@ -23,7 +23,7 @@
   ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
   ui:generateLocales='default,en'
   >
-<ui:style gss="false">
+<ui:style gss='false'>
   @external .gwt-TextArea;
 
   .commitMessage {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index a7799ab..a8dd9c5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -485,8 +485,8 @@
         actionsPanel.add(new Anchor(c.getLinkName(), false,
             c.toBranch(new Branch.NameKey(getProjectKey(), k.ref()))));
       }
-      if (k.web_links() != null) {
-        for (WebLinkInfo webLink : Natives.asList(k.web_links())) {
+      if (k.webLinks() != null) {
+        for (WebLinkInfo webLink : Natives.asList(k.webLinks())) {
           actionsPanel.add(webLink.toAnchor());
         }
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 4c82ff3..08ab2c0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -38,7 +38,6 @@
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
@@ -265,7 +264,7 @@
     grid.addHeader(new SmallHeading(Util.C.headingAgreements()));
 
     contributorAgreements = newInheritedBooleanBox();
-    if (Gerrit.getConfig().isUseContributorAgreements()) {
+    if (Gerrit.info().auth().useContributorAgreements()) {
       saveEnabler.listenTo(contributorAgreements);
       grid.add(Util.C.useContributorAgreements(), contributorAgreements);
     }
@@ -306,12 +305,12 @@
       if (box.getValue(i).startsWith(InheritableBoolean.INHERIT.name())) {
         inheritedIndex = i;
       }
-      if (box.getValue(i).startsWith(inheritedBoolean.configured_value().name())) {
+      if (box.getValue(i).startsWith(inheritedBoolean.configuredValue().name())) {
         box.setSelectedIndex(i);
       }
     }
     if (inheritedIndex >= 0) {
-      if (getProjectKey().equals(Gerrit.getConfig().getWildProject())) {
+      if (Gerrit.info().gerrit().isAllProjects(getProjectKey())) {
         if (box.getSelectedIndex() == inheritedIndex) {
           for (int i = 0; i < box.getItemCount(); i++) {
             if (box.getValue(i).equals(InheritableBoolean.FALSE.name())) {
@@ -323,7 +322,7 @@
         box.removeItem(inheritedIndex);
       } else {
         box.setItemText(inheritedIndex, InheritableBoolean.INHERIT.name() + " ("
-            + inheritedBoolean.inherited_value() + ")");
+            + inheritedBoolean.inheritedValue() + ")");
       }
     }
   }
@@ -342,20 +341,20 @@
 
   void display(ConfigInfo result) {
     descTxt.setText(result.description());
-    setBool(contributorAgreements, result.use_contributor_agreements());
-    setBool(signedOffBy, result.use_signed_off_by());
-    setBool(contentMerge, result.use_content_merge());
-    setBool(newChangeForAllNotInTarget, result.create_new_change_for_all_not_in_target());
-    setBool(requireChangeID, result.require_change_id());
-    setSubmitType(result.submit_type());
+    setBool(contributorAgreements, result.useContributorAgreements());
+    setBool(signedOffBy, result.useSignedOffBy());
+    setBool(contentMerge, result.useContentMerge());
+    setBool(newChangeForAllNotInTarget, result.createNewChangeForAllNotInTarget());
+    setBool(requireChangeID, result.requireChangeId());
+    setSubmitType(result.submitType());
     setState(result.state());
-    maxObjectSizeLimit.setText(result.max_object_size_limit().configured_value());
-    if (result.max_object_size_limit().inherited_value() != null) {
+    maxObjectSizeLimit.setText(result.maxObjectSizeLimit().configuredValue());
+    if (result.maxObjectSizeLimit().inheritedValue() != null) {
       effectiveMaxObjectSizeLimit.setVisible(true);
       effectiveMaxObjectSizeLimit.setText(
-          Util.M.effectiveMaxObjectSizeLimit(result.max_object_size_limit().value()));
+          Util.M.effectiveMaxObjectSizeLimit(result.maxObjectSizeLimit().value()));
       effectiveMaxObjectSizeLimit.setTitle(
-          Util.M.globalMaxObjectSizeLimit(result.max_object_size_limit().inherited_value()));
+          Util.M.globalMaxObjectSizeLimit(result.maxObjectSizeLimit().inheritedValue()));
     } else {
       effectiveMaxObjectSizeLimit.setVisible(false);
     }
@@ -674,19 +673,16 @@
 
   public class ProjectDownloadPanel extends DownloadPanel {
     public ProjectDownloadPanel(String project, boolean isAllowsAnonymous) {
-      super(project, null, isAllowsAnonymous);
+      super(project, isAllowsAnonymous);
     }
 
     @Override
     public void populateDownloadCommandLinks() {
       if (!urls.isEmpty()) {
-        if (allowedCommands.contains(DownloadCommand.CHECKOUT)
-            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-          commands.add(cmdLinkfactory.new CloneCommandLink());
-          if (Gerrit.getConfig().getSshdAddress() != null && hasUserName()) {
-            commands.add(
-                cmdLinkfactory.new CloneWithCommitMsgHookCommandLink(getProjectKey()));
-          }
+        commands.add(cmdLinkfactory.new CloneCommandLink());
+        if (Gerrit.getConfig().getSshdAddress() != null && hasUserName()) {
+          commands.add(
+              cmdLinkfactory.new CloneWithCommitMsgHookCommandLink(getProjectKey()));
         }
       }
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index 828352c..0dff684 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -186,7 +186,7 @@
 
       private void addWebLinks(int row, ProjectInfo k) {
         GitwebLink gitWebLink = Gerrit.getGitwebLink();
-        List<WebLinkInfo> webLinks = Natives.asList(k.web_links());
+        List<WebLinkInfo> webLinks = Natives.asList(k.webLinks());
         if (gitWebLink != null || (webLinks != null && !webLinks.isEmpty())) {
           FlowPanel p = new FlowPanel();
           table.setWidget(row, ProjectsTable.C_REPO_BROWSER, p);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
index 21fac46..137ad2b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
@@ -19,7 +19,7 @@
   xmlns:g='urn:import:com.google.gwt.user.client.ui'
   >
 <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
-<ui:style gss="false">
+<ui:style gss='false'>
   .panel {
     position: relative;
     white-space: nowrap;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
index a5243ae..e831e8b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ChangeGlue.java
@@ -42,7 +42,7 @@
       ChangeInfo change,
       ActionInfo action,
       ActionButton button) {
-    RestApi api = ChangeApi.change(change.legacy_id().get()).view(action.id());
+    RestApi api = ChangeApi.change(change.legacyId().get()).view(action.id());
     JavaScriptObject f = get(action.id());
     if (f != null) {
       ActionContext c = ActionContext.create(api);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
index 0e4048d..c092375 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
@@ -29,7 +29,7 @@
 
 class DefaultActions {
   static void invoke(ChangeInfo change, ActionInfo action, RestApi api) {
-    invoke(action, api, callback(PageLinks.toChange(change.legacy_id())));
+    invoke(action, api, callback(PageLinks.toChange(change.legacyId())));
   }
 
   static void invoke(Project.NameKey project, ActionInfo action, RestApi api) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
index ebcafb8..f968fd2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/EditGlue.java
@@ -29,7 +29,7 @@
       ActionInfo action,
       ActionButton button) {
     RestApi api = ChangeApi.edit(
-          change.legacy_id().get())
+          change.legacyId().get())
       .view(action.id());
 
     JavaScriptObject f = get(action.id());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
index fb489cc..d708e8c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/RevisionGlue.java
@@ -29,7 +29,7 @@
       ActionInfo action,
       ActionButton button) {
     RestApi api = ChangeApi.revision(
-          change.legacy_id().get(),
+          change.legacyId().get(),
           revision.name())
       .view(action.id());
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
index 3b05290..a9e1bb3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ActionMessageBox.ui.xml
@@ -19,7 +19,7 @@
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss="false" type='com.google.gerrit.client.change.ActionMessageBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.ActionMessageBox.Style'>
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
     .popup { background-color: trimColor; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index 532f8f7..10d77cb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -36,9 +36,8 @@
 
 class Actions extends Composite {
   private static final String[] CORE = {
-    "abandon", "restore", "revert", "topic",
-    "cherrypick", "submit", "rebase", "message",
-    "publish", "followup", "/"};
+    "abandon", "cherrypick", "followup", "hashtags", "publish",
+    "rebase", "restore", "revert", "submit", "topic", "/"};
 
   interface Binder extends UiBinder<FlowPanel, Actions> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
@@ -66,6 +65,8 @@
   private String branch;
   private String key;
 
+  private boolean rebaseParentNotCurrent = true;
+
   Actions() {
     initWidget(uiBinder.createAndBindUi(this));
     getElement().setId("change_actions");
@@ -77,17 +78,17 @@
     boolean hasUser = Gerrit.isSignedIn();
     RevisionInfo revInfo = info.revision(revision);
     CommitInfo commit = revInfo.commit();
-    changeId = info.legacy_id();
+    changeId = info.legacyId();
     project = info.project();
     subject = commit.subject();
     message = commit.message();
     branch = info.branch();
-    key = info.change_id();
+    key = info.changeId();
     changeInfo = info;
 
     initChangeActions(info, hasUser);
 
-    NativeMap<ActionInfo> actionMap = revInfo.has_actions()
+    NativeMap<ActionInfo> actionMap = revInfo.hasActions()
         ? revInfo.actions()
         : NativeMap.<ActionInfo> create();
     actionMap.copyKeysIntoChildren("id");
@@ -95,7 +96,7 @@
   }
 
   private void initChangeActions(ChangeInfo info, boolean hasUser) {
-    NativeMap<ActionInfo> actions = info.has_actions()
+    NativeMap<ActionInfo> actions = info.hasActions()
         ? info.actions()
         : NativeMap.<ActionInfo> create();
     actions.copyKeysIntoChildren("id");
@@ -130,8 +131,11 @@
 
     a2b(actions, "cherrypick", cherrypick);
     a2b(actions, "rebase", rebase);
+
+    // The rebase button on change screen is always enabled.
+    // It is the "Rebase" button in the RebaseDialog that might be disabled.
+    rebaseParentNotCurrent = rebase.isEnabled();
     if (rebase.isVisible()) {
-      // it is the rebase button in RebaseDialog that the server wants to disable
       rebase.setEnabled(true);
     }
     RevisionInfo revInfo = changeInfo.revision(revision);
@@ -179,16 +183,8 @@
 
   @UiHandler("rebase")
   void onRebase(@SuppressWarnings("unused") ClickEvent e) {
-    boolean enabled = true;
-    RevisionInfo revInfo = changeInfo.revision(revision);
-    if (revInfo.has_actions()) {
-        NativeMap<ActionInfo> actions = revInfo.actions();
-        if (actions.containsKey("rebase")) {
-          enabled = actions.get("rebase").enabled();
-        }
-    }
     RebaseAction.call(rebase, project, changeInfo.branch(), changeId, revision,
-        enabled);
+        rebaseParentNotCurrent);
   }
 
   @UiHandler("submit")
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
index 40d732a..bc5a321 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -17,7 +17,7 @@
 <ui:UiBinder
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style>
+  <ui:style gss='false'>
     @def BUTTON_HEIGHT 14px;
 
     #change_actions {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
index d8236e6..c3539bc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AddFileBox.ui.xml
@@ -19,7 +19,7 @@
     xmlns:u='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style>
+  <ui:style gss='false'>
     .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index d153771..d97634f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -113,6 +113,7 @@
     String label_need();
     String replyBox();
     String selected();
+    String highlight();
     String hashtagName();
   }
 
@@ -131,6 +132,7 @@
   private String base;
   private String revision;
   private ChangeInfo changeInfo;
+  private boolean hasDraftComments;
   private CommentLinkProcessor commentLinkProcessor;
   private EditInfo edit;
 
@@ -294,18 +296,21 @@
   }
 
   private void initReplyButton(ChangeInfo info, String revision) {
-    if (!info.revision(revision).is_edit()) {
+    if (!info.revision(revision).isEdit()) {
       reply.setTitle(Gerrit.getConfig().getReplyTitle());
       reply.setHTML(new SafeHtmlBuilder()
         .openDiv()
         .append(Gerrit.getConfig().getReplyLabel())
         .closeDiv());
+      if (hasDraftComments) {
+        reply.setStyleName(style.highlight());
+      }
       reply.setVisible(true);
     }
   }
 
   private void gotoSibling(final int offset) {
-    if (offset > 0 && changeInfo.current_revision().equals(revision)) {
+    if (offset > 0 && changeInfo.currentRevision().equals(revision)) {
       return;
     }
 
@@ -319,7 +324,7 @@
       if (revision.equals(revisions.get(i).name())) {
         if (0 <= i + offset && i + offset < revisions.length()) {
           Gerrit.display(PageLinks.toChange(
-              new PatchSet.Id(changeInfo.legacy_id(),
+              new PatchSet.Id(changeInfo.legacyId(),
               revisions.get(i + offset)._number())));
           return;
         }
@@ -331,7 +336,7 @@
   private void initIncludedInAction(ChangeInfo info) {
     if (info.status() == Status.MERGED) {
       includedInAction = new IncludedInAction(
-          info.legacy_id(),
+          info.legacyId(),
           style, headerLine, includedIn);
       includedIn.setVisible(true);
     }
@@ -339,7 +344,7 @@
 
   private void initChangeAction(ChangeInfo info) {
     if (info.status() == Status.DRAFT) {
-      NativeMap<ActionInfo> actions = info.has_actions()
+      NativeMap<ActionInfo> actions = info.hasActions()
           ? info.actions()
           : NativeMap.<ActionInfo> create();
       actions.copyKeysIntoChildren("id");
@@ -353,9 +358,9 @@
   private void initRevisionsAction(ChangeInfo info, String revision,
       NativeMap<ActionInfo> actions) {
     int currentPatchSet;
-    if (info.current_revision() != null
-        && info.revisions().containsKey(info.current_revision())) {
-      currentPatchSet = info.revision(info.current_revision())._number();
+    if (info.currentRevision() != null
+        && info.revisions().containsKey(info.currentRevision())) {
+      currentPatchSet = info.revision(info.currentRevision())._number();
     } else {
       JsArray<RevisionInfo> revList = info.revisions().values();
       RevisionInfo.sortRevisionInfoByNumber(revList);
@@ -374,7 +379,7 @@
     patchSetsText.setInnerText(Resources.M.patchSets(
         currentlyViewedPatchSet, currentPatchSet));
     patchSetsAction = new PatchSetsAction(
-        info.legacy_id(), revision,
+        info.legacyId(), revision,
         style, headerLine, patchSets);
 
     RevisionInfo revInfo = info.revision(revision);
@@ -397,20 +402,20 @@
 
   private void initProjectLinks(final ChangeInfo info) {
     projectSettingsLink.setHref(
-        "#" + PageLinks.toProject(info.project_name_key()));
+        "#" + PageLinks.toProject(info.projectNameKey()));
     projectSettings.addDomHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
         if (Hyperlink.impl.handleAsClick((Event) event.getNativeEvent())) {
           event.stopPropagation();
           event.preventDefault();
-          Gerrit.display(PageLinks.toProject(info.project_name_key()));
+          Gerrit.display(PageLinks.toProject(info.projectNameKey()));
         }
       }
     }, ClickEvent.getType());
     projectDashboard.setText(info.project());
     projectDashboard.setTargetHistoryToken(
-        PageLinks.toProjectDefaultDashboard(info.project_name_key()));
+        PageLinks.toProjectDefaultDashboard(info.projectNameKey()));
   }
 
   private void initBranchLink(ChangeInfo info) {
@@ -418,7 +423,7 @@
     branchLink.setTargetHistoryToken(
         PageLinks.toChangeQuery(
             BranchLink.query(
-                info.project_name_key(),
+                info.projectNameKey(),
                 info.status(),
                 info.branch(),
                 null)));
@@ -448,7 +453,7 @@
         reviewMode.setVisible(false);
       }
 
-      if (rev.is_edit()) {
+      if (rev.isEdit()) {
         if (info.hasEditBasedOnCurrentPatchSet()) {
           publishEdit.setVisible(true);
         } else {
@@ -460,11 +465,11 @@
   }
 
   private boolean isEditModeEnabled(ChangeInfo info, RevisionInfo rev) {
-    if (rev.is_edit()) {
+    if (rev.isEdit()) {
       return true;
     }
     if (edit == null) {
-      return revision.equals(info.current_revision());
+      return revision.equals(info.currentRevision());
     }
     return rev._number() == RevisionInfo.findEditParent(
         info.revisions().values());
@@ -776,9 +781,9 @@
   private void loadConfigInfo(final ChangeInfo info, final String base) {
     info.revisions().copyKeysIntoChildren("name");
     if (edit != null) {
-      edit.set_name(edit.commit().commit());
-      info.set_edit(edit);
-      if (edit.has_files()) {
+      edit.setName(edit.commit().commit());
+      info.setEdit(edit);
+      if (edit.hasFiles()) {
         edit.files().copyKeysIntoChildren("path");
       }
       info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
@@ -796,14 +801,14 @@
       if (revision == null) {
         RevisionInfo.sortRevisionInfoByNumber(list);
         RevisionInfo rev = list.get(list.length() - 1);
-        if (rev.is_edit()) {
-          info.set_current_revision(rev.name());
+        if (rev.isEdit()) {
+          info.setCurrentRevision(rev.name());
         }
       } else if (revision.equals("edit") || revision.equals("0")) {
         for (int i = 0; i < list.length(); i++) {
           RevisionInfo r = list.get(i);
-          if (r.is_edit()) {
-            info.set_current_revision(r.name());
+          if (r.isEdit()) {
+            info.setCurrentRevision(r.name());
             break;
           }
         }
@@ -814,7 +819,7 @@
 
     CallbackGroup group = new CallbackGroup();
     Timestamp lastReply = myLastReply(info);
-    if (rev.is_edit()) {
+    if (rev.isEdit()) {
       loadFileList(b, rev, lastReply, group, null, null);
     } else {
       loadDiff(b, rev, lastReply, group);
@@ -828,7 +833,7 @@
 
     RevisionInfoCache.add(changeId, rev);
     ConfigInfoCache.add(info);
-    ConfigInfoCache.get(info.project_name_key(),
+    ConfigInfoCache.get(info.projectNameKey(),
       group.addFinal(new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
         @Override
         protected void preDisplay(Entry result) {
@@ -843,10 +848,10 @@
 
   static Timestamp myLastReply(ChangeInfo info) {
     if (Gerrit.isSignedIn() && info.messages() != null) {
-      int self = Gerrit.getUserAccountInfo()._account_id();
+      int self = Gerrit.getUserAccountInfo()._accountId();
       for (int i = info.messages().length() - 1; i >= 0; i--) {
         MessageInfo m = info.messages().get(i);
-        if (m.author() != null && m.author()._account_id() == self) {
+        if (m.author() != null && m.author()._accountId() == self) {
           return m.date();
         }
       }
@@ -903,16 +908,19 @@
   }
 
   private List<NativeMap<JsArray<CommentInfo>>> loadComments(
-      RevisionInfo rev, CallbackGroup group) {
-    final int id = rev._number();
+      final RevisionInfo rev, CallbackGroup group) {
     final List<NativeMap<JsArray<CommentInfo>>> r = new ArrayList<>(1);
-    ChangeApi.revision(changeId.get(), rev.name())
-      .view("comments")
+    // TODO(dborowitz): Could eliminate this call by adding an option to include
+    // inline comments in the change detail.
+    ChangeApi.comments(changeId.get())
       .get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
         @Override
         public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-          r.add(result);
-          history.addComments(id, result);
+          // Return value is used for populating the file table, so only count
+          // comments for the current revision. Still include all comments in
+          // the history table.
+          r.add(filterForRevision(result, rev._number()));
+          history.addComments(result);
         }
 
         @Override
@@ -922,6 +930,23 @@
     return r;
   }
 
+  private static NativeMap<JsArray<CommentInfo>> filterForRevision(
+      NativeMap<JsArray<CommentInfo>> comments, int id) {
+    NativeMap<JsArray<CommentInfo>> filtered = NativeMap.create();
+    for (String k : comments.keySet()) {
+      JsArray<CommentInfo> allRevisions = comments.get(k);
+      JsArray<CommentInfo> thisRevision = JsArray.createArray().cast();
+      for (int i = 0; i < allRevisions.length(); i++) {
+        CommentInfo c = allRevisions.get(i);
+        if (c.patchSet() == id) {
+          thisRevision.push(c);
+        }
+      }
+      filtered.put(k, thisRevision);
+    }
+    return filtered;
+  }
+
   private List<NativeMap<JsArray<CommentInfo>>> loadDrafts(
       RevisionInfo rev, CallbackGroup group) {
     final List<NativeMap<JsArray<CommentInfo>>> r = new ArrayList<>(1);
@@ -932,6 +957,7 @@
           @Override
           public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
             r.add(result);
+            hasDraftComments = !result.isEmpty();
           }
 
           @Override
@@ -945,7 +971,7 @@
   }
 
   private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
-    if (rev.is_edit()) {
+    if (rev.isEdit()) {
       return;
     }
 
@@ -953,7 +979,7 @@
         group.add(new AsyncCallback<CommitInfo>() {
           @Override
           public void onSuccess(CommitInfo info) {
-            rev.set_commit(info);
+            rev.setCommit(info);
           }
 
           @Override
@@ -993,7 +1019,7 @@
 
   private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
     RevisionInfo rev = resolveRevisionOrPatchSetId(info, revision,
-        info.current_revision());
+        info.currentRevision());
     if (rev != null) {
       revision = rev.name();
       return rev;
@@ -1010,7 +1036,7 @@
       return rev;
     } else {
       new ErrorDialog(
-          Resources.M.changeWithNoRevisions(info.legacy_id().get())).center();
+          Resources.M.changeWithNoRevisions(info.legacyId().get())).center();
       throw new IllegalStateException("no revision, cannot proceed");
     }
   }
@@ -1090,7 +1116,7 @@
 
     star.setValue(info.starred());
     permalink.setHref(ChangeLink.permalink(changeId));
-    permalink.setText(String.valueOf(info.legacy_id()));
+    permalink.setText(String.valueOf(info.legacyId()));
     topic.set(info, revision);
     commit.set(commentLinkProcessor, info, revision);
     related.set(info, revision);
@@ -1102,7 +1128,7 @@
     }
 
     StringBuilder sb = new StringBuilder();
-    sb.append(Util.M.changeScreenTitleId(info.id_abbreviated()));
+    sb.append(Util.M.changeScreenTitleId(info.idAbbreviated()));
     if (info.subject() != null) {
       sb.append(": ");
       sb.append(info.subject());
@@ -1112,8 +1138,8 @@
     // Although this is related to the revision, we can process it early to
     // render it faster.
     if (!info.status().isOpen()
-        || !revision.equals(info.current_revision())
-        || info.revision(revision).is_edit()) {
+        || !revision.equals(info.currentRevision())
+        || info.revision(revision).isEdit()) {
       setVisible(strategy, false);
     }
 
@@ -1123,38 +1149,39 @@
     initRevisionsAction(info, revision, emptyMap);
     quickApprove.setVisible(false);
     actions.reloadRevisionActions(emptyMap);
-  }
 
-  private void renderRevisionInfo(ChangeInfo info,
-      NativeMap<ActionInfo> actionMap) {
     RevisionInfo revisionInfo = info.revision(revision);
-    boolean current = info.status().isOpen()
-        && revision.equals(info.current_revision())
-        && !revisionInfo.is_edit();
+    boolean current = revision.equals(info.currentRevision())
+        && !revisionInfo.isEdit();
 
-    if (revisionInfo.is_edit()) {
+    if (revisionInfo.isEdit()) {
       statusText.setInnerText(Util.C.changeEdit());
-    } else if (!current && info.status() == Change.Status.NEW) {
+    } else if (!current) {
       statusText.setInnerText(Util.C.notCurrent());
       labels.setVisible(false);
     } else {
       statusText.setInnerText(Util.toLongString(info.status()));
     }
 
-    initRevisionsAction(info, revision, actionMap);
-
     if (Gerrit.isSignedIn()) {
-      replyAction = new ReplyAction(info, revision,
+      replyAction = new ReplyAction(info, revision, hasDraftComments,
           style, commentLinkProcessor, reply, quickApprove);
     }
     history.set(commentLinkProcessor, replyAction, changeId, info);
 
-    if (current) {
+    if (current && info.status().isOpen()) {
       quickApprove.set(info, revision, replyAction);
       loadSubmitType(info.status(), isSubmittable(info));
     } else {
       quickApprove.setVisible(false);
     }
+  }
+
+  private void renderRevisionInfo(ChangeInfo info,
+      NativeMap<ActionInfo> actionMap) {
+    initRevisionsAction(info, revision, actionMap);
+    commit.setParentNotCurrent(actionMap.containsKey("rebase")
+        && actionMap.get("rebase").enabled());
     actions.reloadRevisionActions(actionMap);
   }
 
@@ -1176,7 +1203,7 @@
         ? info.owner().name()
         : info.owner().email() != null
         ? info.owner().email()
-        : String.valueOf(info.owner()._account_id()), Change.Status.NEW));
+        : String.valueOf(info.owner()._accountId()), Change.Status.NEW));
   }
 
   private void renderSubmitType(String action) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index 3f18522..830369d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -21,7 +21,7 @@
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss="false" type='com.google.gerrit.client.change.ChangeScreen.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.ChangeScreen.Style'>
     @eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
index bcd8f6b..357f04c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
@@ -47,7 +47,7 @@
 
       @Override
       public void onSend() {
-        ChangeApi.cherrypick(info.legacy_id().get(), revision,
+        ChangeApi.cherrypick(info.legacyId().get(), revision,
             getDestinationBranch(),
             getMessageText(),
             new GerritCallback<ChangeInfo>() {
@@ -55,7 +55,7 @@
               public void onSuccess(ChangeInfo result) {
                 sent = true;
                 hide();
-                Gerrit.display(PageLinks.toChange(result.legacy_id()));
+                Gerrit.display(PageLinks.toChange(result.legacyId()));
               }
 
               @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
index 8cf3a0c..f75f6a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.java
@@ -20,12 +20,10 @@
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
 import com.google.gerrit.client.changes.ChangeInfo.GitPerson;
 import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
-import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.InlineHyperlink;
@@ -111,8 +109,8 @@
     CommitInfo commit = revInfo.commit();
 
     commitName.setText(revision);
-    idText.setText("Change-Id: " + change.change_id());
-    idText.setPreviewText(change.change_id());
+    idText.setText("Change-Id: " + change.changeId());
+    idText.setPreviewText(change.changeId());
 
     formatLink(commit.author(), authorPanel, authorNameEmail, authorDate,
         change);
@@ -127,15 +125,10 @@
     }
 
     setParents(change.project(), revInfo.commit().parents());
+  }
 
+  void setParentNotCurrent(boolean parentNotCurrent) {
     // display the orange ball if parent has moved on (not current)
-    boolean parentNotCurrent = false;
-    if (revInfo.has_actions()) {
-      NativeMap<ActionInfo> actions = revInfo.actions();
-      if (actions.containsKey("rebase")) {
-        parentNotCurrent = actions.get("rebase").enabled();
-      }
-    }
     UIObject.setVisible(parentNotCurrentText, parentNotCurrent);
     parentNotCurrentText.setInnerText(parentNotCurrent ? "\u25CF" : "");
   }
@@ -148,7 +141,7 @@
           gw.getLinkName());
     }
 
-    JsArray<WebLinkInfo> links = revInfo.commit().web_links();
+    JsArray<WebLinkInfo> links = revInfo.commit().webLinks();
     if (links != null) {
       for (WebLinkInfo link : Natives.asList(links)) {
         webLinkPanel.add(link.toAnchor());
@@ -198,7 +191,7 @@
       a.setStyleName(style.parentWebLink());
       panel.add(a);
     }
-    JsArray<WebLinkInfo> links = c.web_links();
+    JsArray<WebLinkInfo> links = c.webLinks();
     if (links != null) {
       for (WebLinkInfo link : Natives.asList(links)) {
         panel.add(link.toAnchor());
@@ -219,7 +212,7 @@
     // only try to fetch the avatar image for author and committer if an avatar
     // plugin is installed, if the change owner has no avatar info assume that
     // no avatar plugin is installed
-    if (change.owner().has_avatar_info()) {
+    if (change.owner().hasAvatarInfo()) {
       AvatarImage avatar;
       if (change.owner().email().equals(person.email())) {
         avatar = new AvatarImage(change.owner());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
index 25671a9..5f476be 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CommitBox.ui.xml
@@ -21,7 +21,7 @@
     xmlns:clippy='urn:import:com.google.gwtexpui.clippy.client'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:image field="toggle" src="moreLess.png"/>
-  <ui:style gss="false" type='com.google.gerrit.client.change.CommitBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.CommitBox.Style'>
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
     .collapsed .scroll { height: 250px }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
index b51fdd7..9e79f752 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DeleteFileBox.ui.xml
@@ -19,7 +19,7 @@
     xmlns:u='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
index b1bb4e0..34e67dd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadAction.java
@@ -30,7 +30,7 @@
       Widget downloadButton) {
     super(style, relativeTo, downloadButton);
     this.downloadBox = new DownloadBox(info, revision,
-        new PatchSet.Id(info.legacy_id(),
+        new PatchSet.Id(info.legacyId(),
             info.revision(revision)._number()));
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
index 49389f3..0303a88 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
@@ -80,7 +80,7 @@
   protected void onLoad() {
     if (fetch == null) {
       if (psId.get() == 0) {
-        ChangeApi.editWithCommands(change.legacy_id().get()).get(
+        ChangeApi.editWithCommands(change.legacyId().get()).get(
             new AsyncCallback<EditInfo>() {
           @Override
           public void onSuccess(EditInfo result) {
@@ -93,9 +93,9 @@
           }
         });
       } else {
-        RestApi call = ChangeApi.detail(change.legacy_id().get());
+        RestApi call = ChangeApi.detail(change.legacyId().get());
         ChangeList.addOptions(call, EnumSet.of(
-            revision.equals(change.current_revision())
+            revision.equals(change.currentRevision())
                ? ListChangesOption.CURRENT_REVISION
                : ListChangesOption.ALL_REVISIONS,
             ListChangesOption.DOWNLOAD_COMMANDS));
@@ -268,7 +268,7 @@
     if (scheme != null && scheme != pref.getDownloadUrl()) {
       pref.setDownloadUrl(scheme);
       PreferenceInput in = PreferenceInput.create();
-      in.download_scheme(scheme);
+      in.downloadScheme(scheme);
       AccountApi.self().view("preferences")
           .put(in, new AsyncCallback<JavaScriptObject>() {
             @Override
@@ -303,11 +303,11 @@
       return createObject().cast();
     }
 
-    final void download_scheme(DownloadScheme s) {
-      download_scheme0(s.name());
+    final void downloadScheme(DownloadScheme s) {
+      downloadScheme0(s.name());
     }
 
-    private final native void download_scheme0(String n) /*-{
+    private final native void downloadScheme0(String n) /*-{
       this.download_scheme = n;
     }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
index 7172011..e73c70a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.java
@@ -26,8 +26,6 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLPanel;
 
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 class FileComments extends Composite {
@@ -38,22 +36,15 @@
   @UiField FlowPanel comments;
 
   FileComments(CommentLinkProcessor clp,
-      PatchSet.Id ps,
+      PatchSet.Id defaultPs,
       String title,
       List<CommentInfo> list) {
     initWidget(uiBinder.createAndBindUi(this));
 
-    path.setTargetHistoryToken(url(ps, list.get(0)));
+    path.setTargetHistoryToken(url(defaultPs, list.get(0)));
     path.setText(title);
-
-    Collections.sort(list, new Comparator<CommentInfo>() {
-      @Override
-      public int compare(CommentInfo a, CommentInfo b) {
-        return a.line() - b.line();
-      }
-    });
     for (CommentInfo c : list) {
-      comments.add(new LineComment(clp, ps, c));
+      comments.add(new LineComment(clp, defaultPs, c));
     }
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml
index 74822b0..e463e95 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileComments.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .box {
     }
     .path {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index 2947be8..dd8df36 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -87,7 +87,7 @@
     String deltaColumn2();
     String inserted();
     String deleted();
-    String removeButton();
+    String restoreDelete();
   }
 
   public static enum Mode {
@@ -516,8 +516,8 @@
       for (int i = 0; i < list.length(); i++) {
         FileInfo info = list.get(i);
         if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
-          inserted += info.lines_inserted();
-          deleted += info.lines_deleted();
+          inserted += info.linesInserted();
+          deleted += info.linesDeleted();
         }
       }
     }
@@ -548,7 +548,7 @@
       if (mode == Mode.REVIEW) {
         sb.openTh().setStyleName(R.css().reviewed()).closeTh();
       } else {
-        sb.openTh().setStyleName(R.css().removeButton()).closeTh();
+        sb.openTh().setStyleName(R.css().restoreDelete()).closeTh();
       }
       sb.openTh().setStyleName(R.css().status()).closeTh();
       sb.openTh().append(Util.C.patchTableColumnName()).closeTh();
@@ -592,20 +592,26 @@
     }
 
     private void columnDeleteRestore(SafeHtmlBuilder sb, FileInfo info) {
-      sb.openTd().setStyleName(R.css().removeButton());
+      sb.openTd().setStyleName(R.css().restoreDelete());
       if (hasUser) {
         if (!Patch.COMMIT_MSG.equals(info.path())) {
           boolean editable = isEditable(info);
-          sb.openElement("button")
-            .setAttribute("title", editable
-                ? Resources.C.removeFileInline()
-                : Resources.C.restoreFileInline())
-            .setAttribute("onclick", (editable ? DELETE : RESTORE)
-                + "(event," + info._row() + ")")
-            .append(new ImageResourceRenderer().render(editable
-                ? Gerrit.RESOURCES.redNot()
-                : Gerrit.RESOURCES.editUndo()))
+          sb.openDiv()
+            .openElement("button")
+            .setAttribute("title", Resources.C.restoreFileInline())
+            .setAttribute("onclick", RESTORE + "(event," + info._row() + ")")
+            .append(new ImageResourceRenderer().render(
+                Gerrit.RESOURCES.editUndo()))
             .closeElement("button");
+          if (editable) {
+            sb.openElement("button")
+              .setAttribute("title", Resources.C.removeFileInline())
+              .setAttribute("onclick", DELETE + "(event," + info._row() + ")")
+              .append(new ImageResourceRenderer().render(
+                  Gerrit.RESOURCES.redNot()))
+              .closeElement("button");
+          }
+          sb.closeDiv();
         }
       }
       sb.closeTd();
@@ -657,10 +663,10 @@
       }
 
       sb.closeAnchor();
-      if (info.old_path() != null) {
+      if (info.oldPath() != null) {
         sb.br();
         sb.openSpan().setStyleName(R.css().renameCopySource())
-          .append(info.old_path())
+          .append(info.oldPath())
           .closeSpan();
       }
       sb.closeTd();
@@ -733,16 +739,16 @@
       sb.openTd().setStyleName(R.css().deltaColumn1());
       if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
         if (showChangeSizeBars) {
-          sb.append(info.lines_inserted() + info.lines_deleted());
+          sb.append(info.linesInserted() + info.linesDeleted());
         } else if (!ChangeType.DELETED.matches(info.status())) {
           if (ChangeType.ADDED.matches(info.status())) {
-            sb.append(info.lines_inserted())
+            sb.append(info.linesInserted())
               .append(" lines");
           } else {
             sb.append("+")
-              .append(info.lines_inserted())
+              .append(info.linesInserted())
               .append(", -")
-              .append(info.lines_deleted());
+              .append(info.linesDeleted());
           }
         }
       }
@@ -753,24 +759,24 @@
       sb.openTd().setStyleName(R.css().deltaColumn2());
       if (showChangeSizeBars
           && !Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
-          && (info.lines_inserted() != 0 || info.lines_deleted() != 0)) {
+          && (info.linesInserted() != 0 || info.linesDeleted() != 0)) {
         int w = 80;
         int t = inserted + deleted;
-        int i = Math.max(5, (int) (((double) w) * info.lines_inserted() / t));
-        int d = Math.max(5, (int) (((double) w) * info.lines_deleted() / t));
+        int i = Math.max(5, (int) (((double) w) * info.linesInserted() / t));
+        int d = Math.max(5, (int) (((double) w) * info.linesDeleted() / t));
 
         sb.setAttribute(
             "title",
-            Util.M.patchTableSize_LongModify(info.lines_inserted(),
-                info.lines_deleted()));
+            Util.M.patchTableSize_LongModify(info.linesInserted(),
+                info.linesDeleted()));
 
-        if (0 < info.lines_inserted()) {
+        if (0 < info.linesInserted()) {
           sb.openDiv()
             .setStyleName(R.css().inserted())
             .setAttribute("style", "width:" + i + "px")
             .closeDiv();
         }
-        if (0 < info.lines_deleted()) {
+        if (0 < info.linesDeleted()) {
           sb.openDiv()
             .setStyleName(R.css().deleted())
             .setAttribute("style", "width:" + d + "px")
@@ -786,7 +792,7 @@
       if (mode == Mode.REVIEW) {
         sb.openTh().setStyleName(R.css().reviewed()).closeTh();
       } else {
-        sb.openTh().setStyleName(R.css().removeButton()).closeTh();
+        sb.openTh().setStyleName(R.css().restoreDelete()).closeTh();
       }
       sb.openTh().setStyleName(R.css().status()).closeTh();
       sb.openTd().closeTd(); // path
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
index 5a7df72..30394d6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FollowUpAction.java
@@ -39,7 +39,7 @@
         new GerritCallback<ChangeInfo>() {
           @Override
           public void onSuccess(ChangeInfo result) {
-            Gerrit.display(PageLinks.toChange(result.legacy_id()));
+            Gerrit.display(PageLinks.toChange(result.legacyId()));
             hide();
           }
         });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
index faba10d..d2afbcf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
@@ -51,6 +51,8 @@
   private static final String REMOVE;
   private static final String DATA_ID = "data-id";
 
+  private boolean canEdit;
+
   static {
     REMOVE = DOM.createUniqueId().replace('-', '_');
     init(REMOVE);
@@ -121,9 +123,10 @@
   }
 
   void set(ChangeInfo info) {
-    this.changeId = info.legacy_id();
+    canEdit = info.hasActions() && info.actions().containsKey("hashtags");
+    this.changeId = info.legacyId();
     display(info);
-    openForm.setVisible(Gerrit.isSignedIn());
+    openForm.setVisible(canEdit);
   }
 
   @UiHandler("openForm")
@@ -165,13 +168,15 @@
               "#" + PageLinks.toChangeQuery("hashtag:\"" + hashtagName + "\""))
           .setAttribute("role", "listitem")
           .append("#").append(hashtagName)
-          .closeAnchor()
-          .openElement("button")
-          .setAttribute("title", "Remove hashtag")
-          .setAttribute("onclick", REMOVE + "(event)")
-          .append("×")
-          .closeElement("button")
-          .closeSpan();
+          .closeAnchor();
+      if (canEdit) {
+        html.openElement("button")
+            .setAttribute("title", "Remove hashtag")
+            .setAttribute("onclick", REMOVE + "(event)")
+            .append("×")
+            .closeElement("button");
+      }
+      html.closeSpan();
       if (itr.hasNext()) {
         html.append(' ');
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
index fbf0645..ba4d6cc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.ui.xml
@@ -19,7 +19,7 @@
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     button.openAdd {
       margin: 3px 3px 0 0;
       float: right;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
index 7635d81..47a870b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/History.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.client.change;
 
-import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
 import com.google.gerrit.client.changes.CommentInfo;
@@ -23,9 +21,7 @@
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.JsArray;
-import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
 
@@ -33,22 +29,15 @@
 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.Set;
 
 class History extends FlowPanel {
   private CommentLinkProcessor clp;
   private ReplyAction replyAction;
   private Change.Id changeId;
 
-  private final Set<Integer> loaded = new HashSet<>();
-  private final Map<AuthorRevision, List<CommentInfo>> byAuthor =
-      new HashMap<>();
-
-  private final List<Integer> toLoad = new ArrayList<>(4);
-  private int active;
+  private final Map<Integer, List<CommentInfo>> byAuthor = new HashMap<>();
 
   void set(CommentLinkProcessor clp, ReplyAction ra,
       Change.Id id, ChangeInfo info) {
@@ -60,9 +49,7 @@
     if (messages != null) {
       for (MessageInfo msg : Natives.asList(messages)) {
         Message ui = new Message(this, msg);
-        if (loaded.contains(msg._revisionNumber())) {
-          ui.addComments(comments(msg));
-        }
+        ui.addComments(comments(msg));
         add(ui);
       }
       autoOpen(ChangeScreen.myLastReply(info));
@@ -99,18 +86,16 @@
     replyAction.onReply(info);
   }
 
-  void addComments(int id, NativeMap<JsArray<CommentInfo>> map) {
-    loaded.add(id);
-
+  void addComments(NativeMap<JsArray<CommentInfo>> map) {
     for (String path : map.keySet()) {
       for (CommentInfo c : Natives.asList(map.get(path))) {
         c.path(path);
         if (c.author() != null) {
-          AuthorRevision k = new AuthorRevision(c.author(), id);
-          List<CommentInfo> l = byAuthor.get(k);
+          int authorId = c.author()._accountId();
+          List<CommentInfo> l = byAuthor.get(authorId);
           if (l == null) {
             l = new ArrayList<>();
-            byAuthor.put(k, l);
+            byAuthor.put(authorId, l);
           }
           l.add(c);
         }
@@ -118,58 +103,13 @@
     }
   }
 
-  void load(int revisionNumber) {
-    if (revisionNumber > 0 && loaded.add(revisionNumber)) {
-      toLoad.add(revisionNumber);
-      start();
-    }
-  }
-
-  private void start() {
-    if (active >= 2 || toLoad.isEmpty() || !isAttached()) {
-      return;
-    }
-
-    final int revisionNumber = toLoad.remove(0);
-    active++;
-    ChangeApi.revision(new PatchSet.Id(changeId, revisionNumber))
-      .view("comments")
-      .get(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-        @Override
-        public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-          addComments(revisionNumber, result);
-          update(revisionNumber);
-          --active;
-          start();
-        }
-
-        @Override
-        public void onFailure(Throwable caught) {
-          loaded.remove(revisionNumber);
-          loaded.removeAll(toLoad);
-          toLoad.clear();
-          active--;
-        }
-      });
-  }
-
-  private void update(int revisionNumber) {
-    for (Widget child : getChildren()) {
-      Message ui = (Message) child;
-      MessageInfo info = ui.getMessageInfo();
-      if (info._revisionNumber() == revisionNumber) {
-        ui.addComments(comments(info));
-      }
-    }
-  }
-
   private List<CommentInfo> comments(MessageInfo msg) {
     if (msg.author() == null) {
       return Collections.emptyList();
     }
 
-    AuthorRevision k = new AuthorRevision(msg.author(), msg._revisionNumber());
-    List<CommentInfo> list = byAuthor.get(k);
+    int authorId = msg.author()._accountId();
+    List<CommentInfo> list = byAuthor.get(authorId);
     if (list == null) {
       return Collections.emptyList();
     }
@@ -187,34 +127,10 @@
     if (match.isEmpty()) {
       return Collections.emptyList();
     } else if (other.isEmpty()) {
-      byAuthor.remove(k);
+      byAuthor.remove(authorId);
     } else {
-      byAuthor.put(k, other);
+      byAuthor.put(authorId, other);
     }
     return match;
   }
-
-  private static final class AuthorRevision {
-    final int author;
-    final int revision;
-
-    AuthorRevision(AccountInfo author, int revision) {
-      this.author = author._account_id();
-      this.revision = revision;
-    }
-
-    @Override
-    public int hashCode() {
-      return author * 31 + revision;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (!(o instanceof AuthorRevision)) {
-        return false;
-      }
-      AuthorRevision b = (AuthorRevision) o;
-      return author == b.author && revision == b.revision;
-    }
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
index 88ec53f..e59420c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/IncludedInBox.ui.xml
@@ -17,7 +17,7 @@
 <ui:UiBinder
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss="false" type='com.google.gerrit.client.change.IncludedInBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.IncludedInBox.Style'>
     .includedInBox {
       min-width: 300px;
       max-width: 580px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
index 0a02b48..f192a71 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -143,7 +143,7 @@
 
       String val = LabelValue.formatValue(v.shortValue());
       html.openSpan();
-      html.setAttribute("title", label.value_text(val));
+      html.setAttribute("title", label.valueText(val));
       if (v.intValue() == approved) {
         html.setStyleName(style.label_ok());
       } else if (v.intValue() == rejected) {
@@ -172,12 +172,12 @@
 
   private static boolean isApproved(LabelInfo label, ApprovalInfo ai) {
     return label.approved() != null
-        && label.approved()._account_id() == ai._account_id();
+        && label.approved()._accountId() == ai._accountId();
   }
 
   private static boolean isRejected(LabelInfo label, ApprovalInfo ai) {
     return label.rejected() != null
-        && label.rejected()._account_id() == ai._account_id();
+        && label.rejected()._accountId() == ai._accountId();
   }
 
   private String getStyleForLabel(LabelInfo label) {
@@ -234,12 +234,12 @@
       } else if (ai.email() != null) {
         name = ai.email();
       } else {
-        name = Integer.toString(ai._account_id());
+        name = Integer.toString(ai._accountId());
       }
 
       String votableCategories = "";
       if (votable != null) {
-        Set<String> s = votable.get(ai._account_id()).votableLabels();
+        Set<String> s = votable.get(ai._accountId()).votableLabels();
         if (!s.isEmpty()) {
           StringBuilder sb = new StringBuilder(Util.C.votable());
           sb.append(" ");
@@ -254,7 +254,7 @@
       }
       html.openSpan()
           .setAttribute("role", "listitem")
-          .setAttribute(DATA_ID, ai._account_id())
+          .setAttribute(DATA_ID, ai._accountId())
           .setAttribute("title", getTitle(ai, votableCategories))
           .setStyleName(style.label_user());
       if (img != null) {
@@ -270,7 +270,7 @@
         html.closeSelf();
       }
       html.append(name);
-      if (removable.contains(ai._account_id())) {
+      if (removable.contains(ai._accountId())) {
         html.openElement("button")
             .setAttribute("title", Util.M.removeReviewer(name))
             .setAttribute("onclick", REMOVE + "(event)")
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
index 8fa5a68..2d5dce0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.java
@@ -33,15 +33,30 @@
   interface Binder extends UiBinder<HTMLPanel, LineComment> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
+  @UiField Element psLoc;
+  @UiField Element psNum;
   @UiField Element fileLoc;
   @UiField Element lineLoc;
   @UiField InlineHyperlink line;
   @UiField Element message;
 
-  LineComment(CommentLinkProcessor clp, PatchSet.Id ps, CommentInfo info) {
+  LineComment(CommentLinkProcessor clp,
+      PatchSet.Id defaultPs,
+      CommentInfo info) {
     initWidget(uiBinder.createAndBindUi(this));
 
-    if (info.has_line()) {
+    PatchSet.Id ps;
+    if (info.patchSet() != defaultPs.get()) {
+      ps = new PatchSet.Id(defaultPs.getParentKey(), info.patchSet());
+      psNum.setInnerText(Integer.toString(info.patchSet()));
+    } else {
+      ps = defaultPs;
+      psLoc.removeFromParent();
+      psLoc = null;
+      psNum= null;
+    }
+
+    if (info.hasLine()) {
       fileLoc.removeFromParent();
       fileLoc = null;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml
index 3a7f375..2890832 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LineComment.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .box {
       position: relative;
     }
@@ -29,13 +29,16 @@
       font-weight: bold;
     }
     .message {
-      margin-left: 111px;
+      margin-left: 135px;
     }
   </ui:style>
 
   <g:HTMLPanel styleName='{style.box}'>
-    <div class='{style.location}' ui:field='fileLoc'><ui:msg>File Comment</ui:msg></div>
-    <div class='{style.location}' ui:field='lineLoc'><ui:msg>Line <c:InlineHyperlink ui:field='line'/>:</ui:msg></div>
+    <div class='{style.location}'>
+      <span ui:field='psLoc'><ui:msg>PS<span ui:field='psNum'/>, </ui:msg></span>
+      <span ui:field='fileLoc'><ui:msg>File Comment</ui:msg></span>
+      <span ui:field='lineLoc'><ui:msg>Line <c:InlineHyperlink ui:field='line'/>:</ui:msg></span>
+    </div>
     <div class='{style.message}' ui:field='message'/>
   </g:HTMLPanel>
 </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
index 22d39a7..bc1ac30 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.java
@@ -121,13 +121,9 @@
   }
 
   void setOpen(boolean open) {
-    if (open && info._revisionNumber() > 0) {
-      if (commentList == null) {
-        history.load(info._revisionNumber());
-      } else if (!commentList.isEmpty()) {
-        renderComments(commentList);
-        commentList = Collections.emptyList();
-      }
+    if (open && info._revisionNumber() > 0 && !commentList.isEmpty()) {
+      renderComments(commentList);
+      commentList = Collections.emptyList();
     }
     setName(open);
 
@@ -156,7 +152,6 @@
   void autoOpen() {
     if (commentList == null) {
       autoOpen = true;
-      history.load(info._revisionNumber());
     } else if (!commentList.isEmpty()) {
       setOpen(true);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
index def9a5a..e362c07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Message.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gerrit.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss="false" type='com.google.gerrit.client.change.Message.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.Message.Style'>
     .messageBox {
       position: relative;
       width: 1168px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
index 92f6956..f513356 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.java
@@ -142,7 +142,7 @@
         @Override
         public void onSuccess(ChangeInfo result) {
           if (edit != null) {
-            edit.set_name(edit.commit().commit());
+            edit.setName(edit.commit().commit());
             result.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
           }
           render(result.revisions());
@@ -201,7 +201,7 @@
     if (r.draft()) {
       sb.append(Resources.C.draft()).append(' ');
     }
-    if (r.has_draft_comments()) {
+    if (r.hasDraftComments()) {
       sb.openSpan()
         .addStyleName(style.draft_comment())
         .setAttribute("title", Resources.C.draftCommentsTooltip())
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml
index 84d6d7d..7537aa4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PatchSetsBox.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss="false" type='com.google.gerrit.client.change.PatchSetsBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.PatchSetsBox.Style'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
 
     .revisionBox {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
index 6664909..105a3c8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.changes.ReviewInput;
+import com.google.gerrit.client.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.common.PageLinks;
@@ -41,12 +42,12 @@
   }
 
   void set(ChangeInfo info, String commit, ReplyAction action) {
-    if (!info.has_permitted_labels() || !info.status().isOpen()) {
+    if (!info.hasPermittedLabels() || !info.status().isOpen()) {
       // Quick approve needs at least one label on an open change.
       setVisible(false);
       return;
     }
-    if (info.revision(commit).is_edit() || info.revision(commit).draft()) {
+    if (info.revision(commit).isEdit() || info.revision(commit).draft()) {
       setVisible(false);
       return;
     }
@@ -57,11 +58,11 @@
 
     int index = info.getMissingLabelIndex();
     if (index != -1) {
-      LabelInfo label = Natives.asList(info.all_labels().values()).get(index);
-      JsArrayString values = info.permitted_values(label.name());
+      LabelInfo label = Natives.asList(info.allLabels().values()).get(index);
+      JsArrayString values = info.permittedValues(label.name());
       String s = values.get(values.length() - 1);
       short v = LabelInfo.parseValue(s);
-      if (v > 0 && s.equals(label.max_value())) {
+      if (v > 0 && s.equals(label.maxValue())) {
         qName = label.name();
         qValueStr = s;
         qValue = v;
@@ -69,9 +70,10 @@
     }
 
     if (qName != null) {
-      changeId = info.legacy_id();
+      changeId = info.legacyId();
       revision = commit;
       input = ReviewInput.create();
+      input.drafts(DraftHandling.PUBLISH_ALL_REVISIONS);
       input.label(qName, qValue);
       replyAction = action;
       setText(qName + qValueStr);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index 84936ec..0020c50 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -180,7 +180,7 @@
       setForOpenChange(info, revision);
     }
 
-    ChangeApi.revision(info.legacy_id().get(), revision).view("related")
+    ChangeApi.revision(info.legacyId().get(), revision).view("related")
         .get(new TabCallback<RelatedInfo>(Tab.RELATED_CHANGES, info.project(), revision) {
               @Override
               public JsArray<ChangeAndCommit> convert(RelatedInfo result) {
@@ -190,8 +190,8 @@
 
     StringBuilder cherryPicksQuery = new StringBuilder();
     cherryPicksQuery.append(op("project", info.project()));
-    cherryPicksQuery.append(" ").append(op("change", info.change_id()));
-    cherryPicksQuery.append(" ").append(op("-change", info.legacy_id().get()));
+    cherryPicksQuery.append(" ").append(op("change", info.changeId()));
+    cherryPicksQuery.append(" ").append(op("-change", info.legacyId().get()));
     cherryPicksQuery.append(" -is:abandoned");
     ChangeList.query(cherryPicksQuery.toString(),
         EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
@@ -215,7 +215,7 @@
       StringBuilder conflictsQuery = new StringBuilder();
       conflictsQuery.append("status:open");
       conflictsQuery.append(" is:mergeable");
-      conflictsQuery.append(" ").append(op("conflicts", info.legacy_id().get()));
+      conflictsQuery.append(" ").append(op("conflicts", info.legacyId().get()));
       ChangeList.query(conflictsQuery.toString(),
           EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT),
           new TabChangeListCallback(Tab.CONFLICTING_CHANGES, info.project(), revision));
@@ -323,16 +323,16 @@
     protected JsArray<ChangeAndCommit> convert(ChangeList l) {
       JsArray<ChangeAndCommit> arr = JavaScriptObject.createArray().cast();
       for (ChangeInfo i : Natives.asList(l)) {
-        if (i.current_revision() != null && i.revisions().containsKey(i.current_revision())) {
-          RevisionInfo currentRevision = i.revision(i.current_revision());
+        if (i.currentRevision() != null && i.revisions().containsKey(i.currentRevision())) {
+          RevisionInfo currentRevision = i.revision(i.currentRevision());
           ChangeAndCommit c = ChangeAndCommit.create();
-          c.set_id(i.id());
-          c.set_commit(currentRevision.commit());
-          c.set_change_number(i.legacy_id().get());
-          c.set_revision_number(currentRevision._number());
-          c.set_branch(i.branch());
-          c.set_project(i.project());
-          c.set_submittable(i.submittable() && i.mergeable());
+          c.setId(i.id());
+          c.setCommit(currentRevision.commit());
+          c.setChangeNumber(i.legacyId().get());
+          c.setRevisionNumber(currentRevision._number());
+          c.setBranch(i.branch());
+          c.setProject(i.project());
+          c.setSubmittable(i.submittable() && i.mergeable());
           arr.push(c);
         }
       }
@@ -357,56 +357,56 @@
     final native String project() /*-{ return this.project }-*/;
     final native boolean submittable() /*-{ return this._submittable ? true : false; }-*/;
 
-    final native void set_id(String i)
+    final native void setId(String i)
     /*-{ if(i)this.change_id=i; }-*/;
 
-    final native void set_commit(CommitInfo c)
+    final native void setCommit(CommitInfo c)
     /*-{ if(c)this.commit=c; }-*/;
 
-    final native void set_branch(String b)
+    final native void setBranch(String b)
     /*-{ if(b)this.branch=b; }-*/;
 
-    final native void set_project(String b)
+    final native void setProject(String b)
     /*-{ if(b)this.project=b; }-*/;
 
-    public final Change.Id legacy_id() {
-      return has_change_number() ? new Change.Id(_change_number()) : null;
+    public final Change.Id legacyId() {
+      return hasChangeNumber() ? new Change.Id(_changeNumber()) : null;
     }
 
-    public final PatchSet.Id patch_set_id() {
-      return has_change_number() && has_revision_number()
-          ? new PatchSet.Id(legacy_id(), _revision_number())
+    public final PatchSet.Id patchSetId() {
+      return hasChangeNumber() && hasRevisionNumber()
+          ? new PatchSet.Id(legacyId(), _revisionNumber())
           : null;
     }
 
-    public final native boolean has_change_number()
+    public final native boolean hasChangeNumber()
     /*-{ return this.hasOwnProperty('_change_number') }-*/;
 
-    final native boolean has_revision_number()
+    final native boolean hasRevisionNumber()
     /*-{ return this.hasOwnProperty('_revision_number') }-*/;
 
-    final native boolean has_current_revision_number()
+    final native boolean hasCurrentRevisionNumber()
     /*-{ return this.hasOwnProperty('_current_revision_number') }-*/;
 
-    final native int _change_number()
+    final native int _changeNumber()
     /*-{ return this._change_number }-*/;
 
-    final native int _revision_number()
+    final native int _revisionNumber()
     /*-{ return this._revision_number }-*/;
 
-    final native int _current_revision_number()
+    final native int _currentRevisionNumber()
     /*-{ return this._current_revision_number }-*/;
 
-    final native void set_change_number(int n)
+    final native void setChangeNumber(int n)
     /*-{ this._change_number=n; }-*/;
 
-    final native void set_revision_number(int n)
+    final native void setRevisionNumber(int n)
     /*-{ this._revision_number=n; }-*/;
 
-    final native void set_current_revision_number(int n)
+    final native void setCurrentRevisionNumber(int n)
     /*-{ this._current_revision_number=n; }-*/;
 
-    final native void set_submittable(boolean s)
+    final native void setSubmittable(boolean s)
     /*-{ this._submittable=s; }-*/;
 
     protected ChangeAndCommit() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
index c77ca56..1a09fc5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChangesTab.java
@@ -302,7 +302,7 @@
 
       sb.openSpan();
       GitwebLink gw = Gerrit.getGitwebLink();
-      if (gw != null && (!info.has_change_number() || !info.has_revision_number())) {
+      if (gw != null && (!info.hasChangeNumber() || !info.hasRevisionNumber())) {
         sb.setStyleName(RelatedChanges.R.css().gitweb());
         sb.setAttribute("title", gw.getLinkName());
         sb.append('\u25CF'); // Unicode 'BLACK CIRCLE'
@@ -310,8 +310,8 @@
         sb.setStyleName(RelatedChanges.R.css().indirect());
         sb.setAttribute("title", Resources.C.indirectAncestor());
         sb.append('~');
-      } else if (info.has_current_revision_number() && info.has_revision_number()
-          && info._current_revision_number() != info._revision_number()) {
+      } else if (info.hasCurrentRevisionNumber() && info.hasRevisionNumber()
+          && info._currentRevisionNumber() != info._revisionNumber()) {
         sb.setStyleName(RelatedChanges.R.css().notCurrent());
         sb.setAttribute("title", Util.C.notCurrent());
         sb.append('\u25CF'); // Unicode 'BLACK CIRCLE'
@@ -328,8 +328,8 @@
     }
 
     private String url() {
-      if (info.has_change_number() && info.has_revision_number()) {
-        PatchSet.Id id = info.patch_set_id();
+      if (info.hasChangeNumber() && info.hasRevisionNumber()) {
+        PatchSet.Id id = info.patchSetId();
         return "#" + PageLinks.toChange(
             id.getParentKey(),
             id.getId());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
index 32d5168..17e8797 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RenameFileBox.ui.xml
@@ -20,7 +20,7 @@
     xmlns:u='urn:import:com.google.gerrit.client.ui'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .cancel { float: right; }
   </ui:style>
   <g:HTMLPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
index 6e40979..cccab34 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyAction.java
@@ -33,6 +33,7 @@
 class ReplyAction {
   private final PatchSet.Id psId;
   private final String revision;
+  private final boolean hasDraftComments;
   private final ChangeScreen.Style style;
   private final CommentLinkProcessor clp;
   private final Widget replyButton;
@@ -47,23 +48,25 @@
   ReplyAction(
       ChangeInfo info,
       String revision,
+      boolean hasDraftComments,
       ChangeScreen.Style style,
       CommentLinkProcessor clp,
       Widget replyButton,
       Widget quickApproveButton) {
     this.psId = new PatchSet.Id(
-        info.legacy_id(),
+        info.legacyId(),
         info.revisions().get(revision)._number());
     this.revision = revision;
+    this.hasDraftComments = hasDraftComments;
     this.style = style;
     this.clp = clp;
     this.replyButton = replyButton;
     this.quickApproveButton = quickApproveButton;
 
-    boolean current = revision.equals(info.current_revision());
-    allLabels = info.all_labels();
-    permittedLabels = current && info.has_permitted_labels()
-        ? info.permitted_labels()
+    boolean current = revision.equals(info.currentRevision());
+    allLabels = info.allLabels();
+    permittedLabels = current && info.hasPermittedLabels()
+        ? info.permittedLabels()
         : NativeMap.<JsArrayString> create();
   }
 
@@ -111,11 +114,15 @@
       public void onClose(CloseEvent<PopupPanel> event) {
         if (popup == p) {
           popup = null;
+          if (hasDraftComments || replyBox.hasMessage()) {
+            replyButton.setStyleName(style.highlight());
+          }
         }
       }
     });
     p.add(replyBox);
     Window.scrollTo(0, 0);
+    replyButton.removeStyleName(style.highlight());
     p.showRelativeTo(replyButton);
     GlobalKey.dialog(p);
     popup = p;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index f9054fe..aec617a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -21,9 +21,9 @@
 import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
-import com.google.gerrit.client.changes.CommentApi;
 import com.google.gerrit.client.changes.CommentInfo;
 import com.google.gerrit.client.changes.ReviewInput;
+import com.google.gerrit.client.changes.ReviewInput.DraftHandling;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.NativeMap;
@@ -140,19 +140,19 @@
   protected void onLoad() {
     commentsPanel.setVisible(false);
     post.setEnabled(false);
-    CommentApi.drafts(psId, new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
-      @Override
-      public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
-        attachComments(result);
-        displayComments(result);
-        post.setEnabled(true);
-      }
+    ChangeApi.drafts(psId.getParentKey().get())
+        .get(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() {
+          @Override
+          public void onSuccess(NativeMap<JsArray<CommentInfo>> result) {
+            displayComments(result);
+            post.setEnabled(true);
+          }
 
-      @Override
-      public void onFailure(Throwable caught) {
-        post.setEnabled(true);
-      }
-    });
+          @Override
+          public void onFailure(Throwable caught) {
+            post.setEnabled(true);
+          }
+        });
 
     Scheduler.get().scheduleDeferred(new ScheduledCommand() {
       @Override
@@ -180,8 +180,15 @@
     postReview();
   }
 
+  boolean hasMessage() {
+    return !message.getText().trim().isEmpty();
+  }
+
   private void postReview() {
     in.message(message.getText().trim());
+    // Don't send any comments in the request; just publish everything, even if
+    // e.g. a draft was modified in another tab since we last looked it up.
+    in.drafts(DraftHandling.PUBLISH_ALL_REVISIONS);
     in.prePost();
     ChangeApi.revision(psId.getParentKey().get(), revision)
       .view("review")
@@ -284,7 +291,7 @@
     List<LabelAndValues> checkboxes = new ArrayList<>(labels.size());
     int row = 1;
     for (LabelAndValues lv : labels) {
-      if (isCheckBox(lv.info.value_set())) {
+      if (isCheckBox(lv.info.valueSet())) {
         checkboxes.add(lv);
       } else {
         renderRadio(row++, columns, lv);
@@ -321,7 +328,7 @@
     fmt.setStyleName(row, labelHelpColumn, style.label_help());
 
     ApprovalInfo self = Gerrit.isSignedIn()
-        ? lv.info.for_user(Gerrit.getUserAccount().getId().get())
+        ? lv.info.forUser(Gerrit.getUserAccount().getId().get())
         : null;
 
     final LabelRadioGroup group =
@@ -329,7 +336,7 @@
     for (int i = 0; i < columns.size(); i++) {
       Short v = columns.get(i);
       if (lv.permitted.contains(v)) {
-        String text = lv.info.value_text(LabelValue.formatValue(v));
+        String text = lv.info.valueText(LabelValue.formatValue(v));
         LabelRadioButton b = new LabelRadioButton(group, text, v);
         if ((self != null && v == self.value()) || (self == null && v.equals(dv))) {
           b.setValue(true);
@@ -345,7 +352,7 @@
 
   private void renderCheckBox(int row, LabelAndValues lv) {
     ApprovalInfo self = Gerrit.isSignedIn()
-        ? lv.info.for_user(Gerrit.getUserAccount().getId().get())
+        ? lv.info.forUser(Gerrit.getUserAccount().getId().get())
         : null;
 
     final String id = lv.info.name();
@@ -366,7 +373,7 @@
 
     CellFormatter fmt = labelsTable.getCellFormatter();
     fmt.setStyleName(row, labelHelpColumn, style.label_help());
-    labelsTable.setText(row, labelHelpColumn, lv.info.value_text("+1"));
+    labelsTable.setText(row, labelHelpColumn, lv.info.valueText("+1"));
   }
 
   private static boolean isCheckBox(Set<Short> values) {
@@ -375,11 +382,6 @@
         && values.contains((short) 1);
   }
 
-  private void attachComments(NativeMap<JsArray<CommentInfo>> result) {
-    in.drafts(ReviewInput.DraftHandling.KEEP);
-    in.comments(result);
-  }
-
   private void displayComments(NativeMap<JsArray<CommentInfo>> m) {
     comments.clear();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
index 2fd2109..52f6b6a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss="false" type='com.google.gerrit.client.change.ReplyBox.Styles'>
+  <ui:style gss='false' type='com.google.gerrit.client.change.ReplyBox.Styles'>
     .replyBox {
     }
     .label_name {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
index 47d6cad..fe0e6d0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
@@ -48,7 +48,7 @@
               public void onSuccess(ChangeInfo result) {
                 sent = true;
                 hide();
-                Gerrit.display(PageLinks.toChange(result.legacy_id()));
+                Gerrit.display(PageLinks.toChange(result.legacyId()));
               }
 
               @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index 18e5e87..ce35747 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -100,7 +100,7 @@
   }
 
   void set(ChangeInfo info) {
-    this.changeId = info.legacy_id();
+    this.changeId = info.legacyId();
     display(info);
     reviewerSuggestOracle.setChange(changeId);
     openForm.setVisible(Gerrit.isSignedIn());
@@ -125,7 +125,7 @@
 
   @UiHandler("addMe")
   void onAddMe(@SuppressWarnings("unused") ClickEvent e) {
-    String accountId = String.valueOf(Gerrit.getUserAccountInfo()._account_id());
+    String accountId = String.valueOf(Gerrit.getUserAccountInfo()._accountId());
     addReviewer(accountId, false);
   }
 
@@ -198,22 +198,22 @@
   private void display(ChangeInfo info) {
     Map<Integer, AccountInfo> r = new HashMap<>();
     Map<Integer, AccountInfo> cc = new HashMap<>();
-    for (LabelInfo label : Natives.asList(info.all_labels().values())) {
+    for (LabelInfo label : Natives.asList(info.allLabels().values())) {
       if (label.all() != null) {
         for (ApprovalInfo ai : Natives.asList(label.all())) {
-          (ai.value() != 0 ? r : cc).put(ai._account_id(), ai);
+          (ai.value() != 0 ? r : cc).put(ai._accountId(), ai);
         }
       }
     }
     for (Integer i : r.keySet()) {
       cc.remove(i);
     }
-    cc.remove(info.owner()._account_id());
+    cc.remove(info.owner()._accountId());
 
     Set<Integer> removable = new HashSet<>();
-    if (info.removable_reviewers() != null) {
-      for (AccountInfo a : Natives.asList(info.removable_reviewers())) {
-        removable.add(a._account_id());
+    if (info.removableReviewers() != null) {
+      for (AccountInfo a : Natives.asList(info.removableReviewers())) {
+        removable.add(a._accountId());
       }
     }
 
@@ -227,8 +227,8 @@
     reviewersText.setInnerSafeHtml(rHtml);
     ccText.setInnerSafeHtml(ccHtml);
     if (Gerrit.isSignedIn()) {
-      int currentUser = Gerrit.getUserAccountInfo()._account_id();
-      boolean showAddMeButton = info.owner()._account_id() != currentUser
+      int currentUser = Gerrit.getUserAccountInfo()._accountId();
+      boolean showAddMeButton = info.owner()._accountId() != currentUser
           && !cc.containsKey(currentUser)
           && !r.containsKey(currentUser);
       addMe.setVisible(showAddMeButton);
@@ -241,13 +241,13 @@
       LabelInfo label = change.label(name);
       if (label.all() != null) {
         for (ApprovalInfo ai : Natives.asList(label.all())) {
-          int id = ai._account_id();
+          int id = ai._accountId();
           VotableInfo ad = d.get(id);
           if (ad == null) {
             ad = new VotableInfo();
             d.put(id, ad);
           }
-          if (ai.has_value()) {
+          if (ai.hasValue()) {
             ad.votable(name);
           }
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
index 06dfaf5..22e35e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
@@ -20,7 +20,7 @@
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:u='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     button.openAdd {
       margin: 3px 3px 0 0;
       float: right;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
index 09d3476..f45b983 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
@@ -27,7 +27,7 @@
 class SubmitAction {
   static void call(ChangeInfo changeInfo, RevisionInfo revisionInfo) {
     if (ChangeGlue.onSubmitChange(changeInfo, revisionInfo)) {
-      final Change.Id changeId = changeInfo.legacy_id();
+      final Change.Id changeId = changeInfo.legacyId();
       ChangeApi.submit(
         changeId.get(), revisionInfo.name(),
         new GerritCallback<SubmitInfo>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
index 9f45678..b3eff93 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
@@ -68,12 +68,10 @@
   }
 
   void set(ChangeInfo info, String revision) {
-    canEdit = info.has_actions()
-        && info.actions().containsKey("topic")
-        && info.actions().get("topic").enabled();
+    canEdit = info.hasActions() && info.actions().containsKey("topic");
 
     psId = new PatchSet.Id(
-        info.legacy_id(),
+        info.legacyId(),
         info.revisions().get(revision)._number());
 
     initTopicLink(info);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
index 3e20b88..e7e24b4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.ui.xml
@@ -21,7 +21,7 @@
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .show { cursor: pointer; }
     .edit, .cancel { float: right; }
   </ui:style>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
index cdbc693..2daa9ea 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.java
@@ -48,7 +48,7 @@
     HashSet<Integer> seen = new HashSet<>();
     StringBuilder r = new StringBuilder();
     for (MessageInfo m : newMessages) {
-      int a = m.author() != null ? m.author()._account_id() : 0;
+      int a = m.author() != null ? m.author()._accountId() : 0;
       if (seen.add(a)) {
         if (r.length() > 0) {
           r.append(", ");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
index 57c1a77..1d5592b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/UpdateAvailableBar.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .popup {
       position: fixed;
       bottom: 0;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
index 2803db3..f0101cb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -13,14 +13,14 @@
  * limitations under the License.
  */
 
-.pointer, .reviewed, .removeButton {
+.pointer, .reviewed, .restoreDelete {
   padding: 0px;
   vertical-align: top;
 }
 .pointer {
   width: 12px;
 }
-.reviewed, .removeButton {
+.reviewed {
   height: 19px;
   width: 20px;
 }
@@ -96,7 +96,11 @@
   background-color: #d44;
 }
 
-.removeButton button {
+.restoreDelete div {
+  white-space: nowrap;
+}
+
+.restoreDelete button {
   cursor: pointer;
   padding: 0;
   margin: 0 0 0 5px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index dd6c41c..9974532 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -30,8 +30,16 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.EnumSet;
+import java.util.Set;
 
 public class AccountDashboardScreen extends Screen implements ChangeListScreen {
+  private static final Set<ListChangesOption> MY_DASHBOARD_OPTIONS;
+  static {
+    EnumSet<ListChangesOption> options = EnumSet.copyOf(ChangeTable.OPTIONS);
+    options.add(ListChangesOption.REVIEWED);
+    MY_DASHBOARD_OPTIONS = Collections.unmodifiableSet(options);
+  }
+
   private final Account.Id ownerId;
   private final boolean mine;
   private ChangeTable table;
@@ -96,16 +104,14 @@
     super.onLoad();
 
     String who = mine ? "self" : ownerId.toString();
-    ChangeList.query(
+    ChangeList.queryMultiple(
         new ScreenLoadCallback<JsArray<ChangeList>>(this) {
           @Override
           protected void preDisplay(JsArray<ChangeList> result) {
             display(result);
           }
         },
-        mine
-          ? EnumSet.of(ListChangesOption.REVIEWED)
-          : EnumSet.noneOf(ListChangesOption.class),
+        mine ? MY_DASHBOARD_OPTIONS : DashboardTable.OPTIONS,
         queryOutgoing(who),
         queryIncoming(who),
         queryClosed(who) + " -age:4w limit:10");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index 1135491..7a5e239 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -51,7 +51,7 @@
     input.project(emptyToNull(project));
     input.branch(emptyToNull(branch));
     input.subject(emptyToNull(subject));
-    input.base_change(emptyToNull(base));
+    input.baseChange(emptyToNull(base));
 
     if (Gerrit.getConfig().isAllowDraftChanges()) {
       input.status(Change.Status.DRAFT.toString());
@@ -102,6 +102,14 @@
     return call(id, revision, "actions");
   }
 
+  public static RestApi comments(int id) {
+    return call(id, "comments");
+  }
+
+  public static RestApi drafts(int id) {
+    return call(id, "drafts");
+  }
+
   public static void edit(int id, AsyncCallback<EditInfo> cb) {
     edit(id).get(cb);
   }
@@ -179,7 +187,7 @@
   /** Submit a specific revision of a change. */
   public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
     SubmitInput in = SubmitInput.create();
-    in.wait_for_merge(true);
+    in.waitForMerge(true);
     call(id, commit, "submit").post(in, cb);
   }
 
@@ -243,7 +251,7 @@
     public final native void branch(String b) /*-{ if(b)this.branch=b; }-*/;
     public final native void project(String p) /*-{ if(p)this.project=p; }-*/;
     public final native void subject(String s) /*-{ if(s)this.subject=s; }-*/;
-    public final native void base_change(String b) /*-{ if(b)this.base_change=b; }-*/;
+    public final native void baseChange(String b) /*-{ if(b)this.base_change=b; }-*/;
     public final native void status(String s)  /*-{ if(s)this.status=s; }-*/;
 
     protected CreateChangeInput() {
@@ -273,7 +281,7 @@
   }
 
   private static class SubmitInput extends JavaScriptObject {
-    final native void wait_for_merge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
+    final native void waitForMerge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
 
     static SubmitInput create() {
       return (SubmitInput) createObject();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
index a00e329..0928cd8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeEditApi.java
@@ -76,15 +76,15 @@
   public static void rename(int id, String path, String newPath,
       AsyncCallback<VoidResult> cb) {
     Input in = Input.create();
-    in.old_path(path);
-    in.new_path(newPath);
+    in.oldPath(path);
+    in.newPath(newPath);
     ChangeApi.edit(id).post(in, cb);
   }
 
   /** Restore (undo delete/modify) a file in the pending edit. */
   public static void restore(int id, String path, AsyncCallback<VoidResult> cb) {
     Input in = Input.create();
-    in.restore_path(path);
+    in.restorePath(path);
     ChangeApi.edit(id).post(in, cb);
   }
 
@@ -101,9 +101,9 @@
       return createObject().cast();
     }
 
-    final native void restore_path(String p) /*-{ this.restore_path=p }-*/;
-    final native void old_path(String p) /*-{ this.old_path=p }-*/;
-    final native void new_path(String p) /*-{ this.new_path=p }-*/;
+    final native void restorePath(String p) /*-{ this.restore_path=p }-*/;
+    final native void oldPath(String p) /*-{ this.old_path=p }-*/;
+    final native void newPath(String p) /*-{ this.new_path=p }-*/;
 
     protected Input() {
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index a4f2e3f..9052fef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -41,24 +41,24 @@
 
 public class ChangeInfo extends JavaScriptObject {
   public final void init() {
-    if (all_labels() != null) {
-      all_labels().copyKeysIntoChildren("_name");
+    if (allLabels() != null) {
+      allLabels().copyKeysIntoChildren("_name");
     }
   }
 
-  public final Project.NameKey project_name_key() {
+  public final Project.NameKey projectNameKey() {
     return new Project.NameKey(project());
   }
 
-  public final Change.Id legacy_id() {
+  public final Change.Id legacyId() {
     return new Change.Id(_number());
   }
 
   public final Timestamp created() {
-    Timestamp ts = _get_cts();
+    Timestamp ts = _getCts();
     if (ts == null) {
       ts = JavaSqlTimestamp_JsonSerializer.parseTimestamp(createdRaw());
-      _set_cts(ts);
+      _setCts(ts);
     }
     return ts;
   }
@@ -66,18 +66,18 @@
   public final boolean hasEditBasedOnCurrentPatchSet() {
     JsArray<RevisionInfo> revList = revisions().values();
     RevisionInfo.sortRevisionInfoByNumber(revList);
-    return revList.get(revList.length() - 1).is_edit();
+    return revList.get(revList.length() - 1).isEdit();
   }
 
-  private final native Timestamp _get_cts() /*-{ return this._cts; }-*/;
-  private final native void _set_cts(Timestamp ts) /*-{ this._cts = ts; }-*/;
+  private final native Timestamp _getCts() /*-{ return this._cts; }-*/;
+  private final native void _setCts(Timestamp ts) /*-{ this._cts = ts; }-*/;
 
   public final Timestamp updated() {
     return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
   }
 
-  public final String id_abbreviated() {
-    return new Change.Key(change_id()).abbreviate();
+  public final String idAbbreviated() {
+    return new Change.Key(changeId()).abbreviate();
   }
 
   public final Change.Status status() {
@@ -85,14 +85,14 @@
   }
 
   public final Set<String> labels() {
-    return all_labels().keySet();
+    return allLabels().keySet();
   }
 
   public final native String id() /*-{ return this.id; }-*/;
   public final native String project() /*-{ return this.project; }-*/;
   public final native String branch() /*-{ return this.branch; }-*/;
   public final native String topic() /*-{ return this.topic; }-*/;
-  public final native String change_id() /*-{ return this.change_id; }-*/;
+  public final native String changeId() /*-{ return this.change_id; }-*/;
   public final native boolean mergeable() /*-{ return this.mergeable || false; }-*/;
   public final native int insertions() /*-{ return this.insertions; }-*/;
   public final native int deletions() /*-{ return this.deletions; }-*/;
@@ -103,30 +103,30 @@
   private final native String updatedRaw() /*-{ return this.updated; }-*/;
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
-  public final native NativeMap<LabelInfo> all_labels() /*-{ return this.labels; }-*/;
+  public final native NativeMap<LabelInfo> allLabels() /*-{ return this.labels; }-*/;
   public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
-  public final native String current_revision() /*-{ return this.current_revision; }-*/;
-  public final native void set_current_revision(String r) /*-{ this.current_revision = r; }-*/;
-  private final native void set_submittable(boolean x) /*-{ this.submittable = x; }-*/;
+  public final native String currentRevision() /*-{ return this.current_revision; }-*/;
+  public final native void setCurrentRevision(String r) /*-{ this.current_revision = r; }-*/;
+  private final native void setSubmittable(boolean x) /*-{ this.submittable = x; }-*/;
   public final native NativeMap<RevisionInfo> revisions() /*-{ return this.revisions; }-*/;
   public final native RevisionInfo revision(String n) /*-{ return this.revisions[n]; }-*/;
   public final native JsArray<MessageInfo> messages() /*-{ return this.messages; }-*/;
-  public final native void set_edit(EditInfo edit) /*-{ this.edit = edit; }-*/;
+  public final native void setEdit(EditInfo edit) /*-{ this.edit = edit; }-*/;
   public final native EditInfo edit() /*-{ return this.edit; }-*/;
-  public final native boolean has_edit() /*-{ return this.hasOwnProperty('edit') }-*/;
+  public final native boolean hasEdit() /*-{ return this.hasOwnProperty('edit') }-*/;
   public final native JsArrayString hashtags() /*-{ return this.hashtags; }-*/;
 
-  public final native boolean has_permitted_labels()
+  public final native boolean hasPermittedLabels()
   /*-{ return this.hasOwnProperty('permitted_labels') }-*/;
-  public final native NativeMap<JsArrayString> permitted_labels()
+  public final native NativeMap<JsArrayString> permittedLabels()
   /*-{ return this.permitted_labels; }-*/;
-  public final native JsArrayString permitted_values(String n)
+  public final native JsArrayString permittedValues(String n)
   /*-{ return this.permitted_labels[n]; }-*/;
 
-  public final native JsArray<AccountInfo> removable_reviewers()
+  public final native JsArray<AccountInfo> removableReviewers()
   /*-{ return this.removable_reviewers; }-*/;
 
-  public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+  public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
   public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
 
   final native int _number() /*-{ return this._number; }-*/;
@@ -151,14 +151,14 @@
   public final int getMissingLabelIndex() {
     int i = -1;
     int ret = -1;
-    List<LabelInfo> labels = Natives.asList(all_labels().values());
+    List<LabelInfo> labels = Natives.asList(allLabels().values());
     for (LabelInfo label : labels) {
       i++;
-      if (!permitted_labels().containsKey(label.name())) {
+      if (!permittedLabels().containsKey(label.name())) {
         continue;
       }
 
-      JsArrayString values = permitted_values(label.name());
+      JsArrayString values = permittedValues(label.name());
       if (values.length() == 0) {
         continue;
       }
@@ -168,7 +168,7 @@
           if (ret != -1) {
             // more than one label is missing, so it's unclear which to quick
             // approve, return -1
-            set_submittable(false);
+            setSubmittable(false);
             return -1;
           } else {
             ret = i;
@@ -181,11 +181,11 @@
 
         case REJECT: // Submit cannot happen, do not quick approve.
         case IMPOSSIBLE:
-          set_submittable(false);
+          setSubmittable(false);
           return -1;
       }
     }
-    set_submittable(ret == -1);
+    setSubmittable(ret == -1);
     return ret;
   }
 
@@ -213,10 +213,10 @@
     public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
 
     public final native JsArray<ApprovalInfo> all() /*-{ return this.all; }-*/;
-    public final ApprovalInfo for_user(int user) {
+    public final ApprovalInfo forUser(int user) {
       JsArray<ApprovalInfo> all = all();
       for (int i = 0; all != null && i < all.length(); i++) {
-        if (all.get(i)._account_id() == user) {
+        if (all.get(i)._accountId() == user) {
           return all.get(i);
         }
       }
@@ -227,7 +227,7 @@
     public final Set<String> values() {
       return Natives.keys(_values());
     }
-    public final native String value_text(String n) /*-{ return this.values[n]; }-*/;
+    public final native String valueText(String n) /*-{ return this.values[n]; }-*/;
 
     public final native boolean optional() /*-{ return this.optional ? true : false; }-*/;
     public final native boolean blocking() /*-{ return this.blocking ? true : false; }-*/;
@@ -240,11 +240,11 @@
       return 0;
     }-*/;
 
-    public final String max_value() {
-      return LabelValue.formatValue(value_set().last());
+    public final String maxValue() {
+      return LabelValue.formatValue(valueSet().last());
     }
 
-    public final SortedSet<Short> value_set() {
+    public final SortedSet<Short> valueSet() {
       SortedSet<Short> values = new TreeSet<>();
       for (String v : values()) {
         values.add(parseValue(v));
@@ -266,7 +266,7 @@
   }
 
   public static class ApprovalInfo extends AccountInfo {
-    public final native boolean has_value() /*-{ return this.hasOwnProperty('value'); }-*/;
+    public final native boolean hasValue() /*-{ return this.hasOwnProperty('value'); }-*/;
     public final native short value() /*-{ return this.value || 0; }-*/;
 
     protected ApprovalInfo() {
@@ -275,17 +275,17 @@
 
   public static class EditInfo extends JavaScriptObject {
     public final native String name() /*-{ return this.name; }-*/;
-    public final native String set_name(String n) /*-{ this.name = n; }-*/;
-    public final native String base_revision() /*-{ return this.base_revision; }-*/;
+    public final native String setName(String n) /*-{ this.name = n; }-*/;
+    public final native String baseRevision() /*-{ return this.base_revision; }-*/;
     public final native CommitInfo commit() /*-{ return this.commit; }-*/;
 
-    public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+    public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
     public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
 
-    public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
+    public final native boolean hasFetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
     public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
 
-    public final native boolean has_files() /*-{ return this.hasOwnProperty('files') }-*/;
+    public final native boolean hasFiles() /*-{ return this.hasOwnProperty('files') }-*/;
     public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
 
     protected EditInfo() {
@@ -307,19 +307,19 @@
     public final native int _number() /*-{ return this._number; }-*/;
     public final native String name() /*-{ return this.name; }-*/;
     public final native boolean draft() /*-{ return this.draft || false; }-*/;
-    public final native boolean has_draft_comments() /*-{ return this.has_draft_comments || false; }-*/;
-    public final native boolean is_edit() /*-{ return this._number == 0; }-*/;
+    public final native boolean hasDraftComments() /*-{ return this.has_draft_comments || false; }-*/;
+    public final native boolean isEdit() /*-{ return this._number == 0; }-*/;
     public final native CommitInfo commit() /*-{ return this.commit; }-*/;
-    public final native void set_commit(CommitInfo c) /*-{ this.commit = c; }-*/;
-    public final native String edit_base() /*-{ return this.edit_base; }-*/;
+    public final native void setCommit(CommitInfo c) /*-{ this.commit = c; }-*/;
+    public final native String editBase() /*-{ return this.edit_base; }-*/;
 
-    public final native boolean has_files() /*-{ return this.hasOwnProperty('files') }-*/;
+    public final native boolean hasFiles() /*-{ return this.hasOwnProperty('files') }-*/;
     public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
 
-    public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
+    public final native boolean hasActions() /*-{ return this.hasOwnProperty('actions') }-*/;
     public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
 
-    public final native boolean has_fetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
+    public final native boolean hasFetch() /*-{ return this.hasOwnProperty('fetch') }-*/;
     public final native NativeMap<FetchInfo> fetch() /*-{ return this.fetch; }-*/;
 
     public static void sortRevisionInfoByNumber(JsArray<RevisionInfo> list) {
@@ -331,7 +331,7 @@
         }
 
         private int num(RevisionInfo r) {
-          return !r.is_edit() ? 2 * (r._number() - 1) + 1 : 2 * editParent;
+          return !r.isEdit() ? 2 * (r._number() - 1) + 1 : 2 * editParent;
         }
       });
     }
@@ -340,8 +340,8 @@
       for (int i = 0; i < list.length(); i++) {
         // edit under revisions?
         RevisionInfo editInfo = list.get(i);
-        if (editInfo.is_edit()) {
-          String parentRevision = editInfo.edit_base();
+        if (editInfo.isEdit()) {
+          String parentRevision = editInfo.editBase();
           // find parent
           for (int j = 0; j < list.length(); j++) {
             RevisionInfo parentInfo = list.get(j);
@@ -381,7 +381,7 @@
     public final native GitPerson committer() /*-{ return this.committer; }-*/;
     public final native String subject() /*-{ return this.subject; }-*/;
     public final native String message() /*-{ return this.message; }-*/;
-    public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+    public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
     protected CommitInfo() {
     }
@@ -415,7 +415,7 @@
   }
 
   public static class MergeableInfo extends JavaScriptObject {
-    public final native String submit_type() /*-{ return this.submit_type }-*/;
+    public final native String submitType() /*-{ return this.submit_type }-*/;
     public final native boolean mergeable() /*-{ return this.mergeable }-*/;
 
     protected MergeableInfo() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
index b1866dd..5fcaf24 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -20,52 +20,68 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
-import java.util.EnumSet;
+import java.util.Set;
 
 /** List of changes available from {@code /changes/}. */
 public class ChangeList extends JsArray<ChangeInfo> {
   private static final String URI = "/changes/";
-  private static final EnumSet<ListChangesOption> OPTIONS = EnumSet.of(
-      ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS);
 
-  /** Run 2 or more queries in a single remote invocation. */
-  public static void query(
-      AsyncCallback<JsArray<ChangeList>> callback,
-      EnumSet<ListChangesOption> options,
+  /** Run multiple queries in a single remote invocation. */
+  public static void queryMultiple(
+      final AsyncCallback<JsArray<ChangeList>> callback,
+      Set<ListChangesOption> options,
       String... queries) {
-    assert queries.length >= 2; // At least 2 is required for correct result.
+    if (queries.length == 0) {
+      return;
+    }
     RestApi call = new RestApi(URI);
     for (String q : queries) {
       call.addParameterRaw("q", KeyUtil.encode(q));
     }
-    OPTIONS.addAll(options);
-    addOptions(call, OPTIONS);
-    call.get(callback);
+    addOptions(call, options);
+    if (queries.length == 1) {
+      // Server unwraps a single query, so wrap it back in an array for the
+      // callback.
+      call.get(new AsyncCallback<ChangeList>() {
+        @Override
+        public void onSuccess(ChangeList result) {
+          JsArray<ChangeList> wrapped = JsArray.createArray(1).cast();
+          wrapped.set(0, result);
+          callback.onSuccess(wrapped);
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          callback.onFailure(caught);
+        }
+      });
+    } else {
+      call.get(callback);
+    }
   }
 
   public static void query(String query,
-      EnumSet<ListChangesOption> options,
+      Set<ListChangesOption> options,
       AsyncCallback<ChangeList> callback) {
-    RestApi call = newQuery(query);
-    addOptions(call, options);
-    call.get(callback);
+    query(query, options, callback, 0, 0);
   }
 
-  public static void next(String query,
-      int start, int limit,
-      AsyncCallback<ChangeList> callback) {
+  public static void query(String query,
+      Set<ListChangesOption> options,
+      AsyncCallback<ChangeList> callback,
+      int start, int limit) {
     RestApi call = newQuery(query);
     if (limit > 0) {
       call.addParameter("n", limit);
     }
-    addOptions(call, OPTIONS);
+    addOptions(call, options);
     if (start != 0) {
       call.addParameter("S", start);
     }
     call.get(callback);
   }
 
-  public static void addOptions(RestApi call, EnumSet<ListChangesOption> s) {
+  public static void addOptions(RestApi call, Set<ListChangesOption> s) {
     call.addParameterRaw("O", Integer.toHexString(ListChangesOption.toBits(s)));
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 8b71448..c52ac32 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ReviewCategoryStrategy;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.dom.client.Element;
@@ -45,9 +46,17 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.List;
+import java.util.Set;
 
 public class ChangeTable extends NavigationTable<ChangeInfo> {
+  // If changing default options, also update in
+  // ChangeIT#defaultSearchDoesNotTouchDatabase().
+  static final Set<ListChangesOption> OPTIONS =
+      Collections.unmodifiableSet(EnumSet.of(
+          ListChangesOption.LABELS, ListChangesOption.DETAILED_ACCOUNTS));
+
   private static final int C_STAR = 1;
   private static final int C_ID = 2;
   private static final int C_SUBJECT = 3;
@@ -118,13 +127,13 @@
 
   @Override
   protected Object getRowItemKey(final ChangeInfo item) {
-    return item.legacy_id();
+    return item.legacyId();
   }
 
   @Override
   protected void onOpenRow(final int row) {
     final ChangeInfo c = getRowItem(row);
-    final Change.Id id = c.legacy_id();
+    final Change.Id id = c.legacyId();
     Gerrit.display(PageLinks.toChange(id));
   }
 
@@ -208,10 +217,10 @@
     CellFormatter fmt = table.getCellFormatter();
     if (Gerrit.isSignedIn()) {
       table.setWidget(row, C_STAR, StarredChanges.createIcon(
-          c.legacy_id(),
+          c.legacyId(),
           c.starred()));
     }
-    table.setWidget(row, C_ID, new TableChangeLink(String.valueOf(c.legacy_id()), c));
+    table.setWidget(row, C_ID, new TableChangeLink(String.valueOf(c.legacyId()), c));
 
     String subject = Util.cropSubject(c.subject());
     table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
@@ -229,8 +238,8 @@
       table.setText(row, C_OWNER, "");
     }
 
-    table.setWidget(row, C_PROJECT, new ProjectLink(c.project_name_key()));
-    table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
+    table.setWidget(row, C_PROJECT, new ProjectLink(c.projectNameKey()));
+    table.setWidget(row, C_BRANCH, new BranchLink(c.projectNameKey(), c
         .status(), c.branch(), c.topic()));
     if (Gerrit.isSignedIn()
         && Gerrit.getUserAccount().getGeneralPreferences()
@@ -447,7 +456,7 @@
 
   private final class TableChangeLink extends ChangeLink {
     private TableChangeLink(final String text, final ChangeInfo c) {
-      super(text, c.legacy_id());
+      super(text, c.legacyId());
     }
 
     @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
index 52e5d40..c69ee57 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentInfo.java
@@ -29,7 +29,7 @@
     n.path(path);
     n.side(side);
     if (range != null) {
-      n.line(range.end_line());
+      n.line(range.endLine());
       n.range(range);
     } else if (line > 0) {
       n.line(line);
@@ -41,11 +41,11 @@
     CommentInfo n = createObject().cast();
     n.path(r.path());
     n.side(r.side());
-    n.in_reply_to(r.id());
-    if (r.has_range()) {
-      n.line(r.range().end_line());
+    n.inReplyTo(r.id());
+    if (r.hasRange()) {
+      n.line(r.range().endLine());
       n.range(r.range());
-    } else if (r.has_line()) {
+    } else if (r.hasLine()) {
       n.line(r.line());
     }
     return n;
@@ -56,12 +56,12 @@
     n.path(s.path());
     n.side(s.side());
     n.id(s.id());
-    n.in_reply_to(s.in_reply_to());
+    n.inReplyTo(s.inReplyTo());
     n.message(s.message());
-    if (s.has_range()) {
-      n.line(s.range().end_line());
+    if (s.hasRange()) {
+      n.line(s.range().endLine());
       n.range(s.range());
-    } else if (s.has_line()) {
+    } else if (s.hasLine()) {
       n.line(s.line());
     }
     return n;
@@ -71,7 +71,7 @@
   public final native void id(String i) /*-{ this.id = i }-*/;
   public final native void line(int n) /*-{ this.line = n }-*/;
   public final native void range(CommentRange r) /*-{ this.range = r }-*/;
-  public final native void in_reply_to(String i) /*-{ this.in_reply_to = i }-*/;
+  public final native void inReplyTo(String i) /*-{ this.in_reply_to = i }-*/;
   public final native void message(String m) /*-{ this.message = m }-*/;
 
   public final void side(Side side) {
@@ -81,7 +81,8 @@
 
   public final native String path() /*-{ return this.path }-*/;
   public final native String id() /*-{ return this.id }-*/;
-  public final native String in_reply_to() /*-{ return this.in_reply_to }-*/;
+  public final native String inReplyTo() /*-{ return this.in_reply_to }-*/;
+  public final native int patchSet() /*-{ return this.patch_set }-*/;
 
   public final Side side() {
     String s = sideRaw();
@@ -108,8 +109,8 @@
 
   public final native AccountInfo author() /*-{ return this.author }-*/;
   public final native int line() /*-{ return this.line || 0 }-*/;
-  public final native boolean has_line() /*-{ return this.hasOwnProperty('line') }-*/;
-  public final native boolean has_range() /*-{ return this.hasOwnProperty('range') }-*/;
+  public final native boolean hasLine() /*-{ return this.hasOwnProperty('line') }-*/;
+  public final native boolean hasRange() /*-{ return this.hasOwnProperty('range') }-*/;
   public final native CommentRange range() /*-{ return this.range }-*/;
   public final native String message() /*-{ return this.message }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
index 9e97c56..26e11ae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
@@ -20,14 +20,12 @@
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.http.client.URL;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
 import java.util.ArrayList;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.ListIterator;
 
@@ -104,33 +102,19 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-
-    if (queries.size() == 1) {
-      ChangeList.next(queries.get(0),
-          0, 0,
-          new GerritCallback<ChangeList>() {
-            @Override
-            public void onSuccess(ChangeList result) {
-              updateColumnsForLabels(result);
-              sections.get(0).display(result);
-              finishDisplay();
+    ChangeList.queryMultiple(
+        new GerritCallback<JsArray<ChangeList>>() {
+          @Override
+          public void onSuccess(JsArray<ChangeList> result) {
+            List<ChangeList> cls = Natives.asList(result);
+            updateColumnsForLabels(cls.toArray(new ChangeList[cls.size()]));
+            for (int i = 0; i < cls.size(); i++) {
+              sections.get(i).display(cls.get(i));
             }
-        });
-    } else if (! queries.isEmpty()) {
-      ChangeList.query(
-          new GerritCallback<JsArray<ChangeList>>() {
-            @Override
-            public void onSuccess(JsArray<ChangeList> result) {
-              List<ChangeList> cls = Natives.asList(result);
-              updateColumnsForLabels(cls.toArray(new ChangeList[cls.size()]));
-              for (int i = 0; i < cls.size(); i++) {
-                sections.get(i).display(cls.get(i));
-              }
-              finishDisplay();
-            }
-          },
-          EnumSet.noneOf(ListChangesOption.class),
-          queries.toArray(new String[queries.size()]));
-    }
+            finishDisplay();
+          }
+        },
+        OPTIONS,
+        queries.toArray(new String[queries.size()]));
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index 488b34b..dddbd61 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -54,7 +54,7 @@
         if (isAttached()) {
           if (result.length() == 1 && isSingleQuery(query)) {
             ChangeInfo c = result.get(0);
-            Change.Id id = c.legacy_id();
+            Change.Id id = c.legacyId();
             Gerrit.display(PageLinks.toChange(id));
           } else {
             display(result);
@@ -74,7 +74,8 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    ChangeList.next(query, start, pageSize, loadCallback());
+    ChangeList.query(
+        query, ChangeTable.OPTIONS, loadCallback(), start, pageSize);
   }
 
   private static boolean isSingleQuery(String query) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
index 7651495..096dbd0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ReviewInput.java
@@ -24,7 +24,7 @@
   }
 
   public static enum DraftHandling {
-    DELETE, PUBLISH, KEEP
+    DELETE, PUBLISH, KEEP, PUBLISH_ALL_REVISIONS
   }
 
   public static ReviewInput create() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/AuthInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/AuthInfo.java
new file mode 100644
index 0000000..5e66d83
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/AuthInfo.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.config;
+
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AuthInfo extends JavaScriptObject {
+  public final AuthType authType() {
+    return AuthType.valueOf(authTypeRaw());
+  }
+
+  public final boolean isOpenId() {
+    return authType() == AuthType.OPENID;
+  }
+
+  public final boolean isOAuth() {
+    return authType() == AuthType.OAUTH;
+  }
+
+  public final boolean isDev() {
+    return authType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT;
+  }
+
+  public final boolean isClientSslCertLdap() {
+    return authType() == AuthType.CLIENT_SSL_CERT_LDAP;
+  }
+
+  public final boolean isCustomExtension() {
+    return authType() == AuthType.CUSTOM_EXTENSION;
+  }
+
+  public final boolean canEdit(Account.FieldName f) {
+    return editableAccountFields().contains(f);
+  }
+
+  public final List<Account.FieldName> editableAccountFields() {
+    List<Account.FieldName> fields = new ArrayList<>();
+    for (AccountFieldNameInfo f : Natives.asList(_editableAccountFields())) {
+      fields.add(f.get());
+    }
+    return fields;
+  }
+
+  public final native boolean useContributorAgreements()
+  /*-{ return this.use_contributor_agreements || false; }-*/;
+  private final native String authTypeRaw() /*-{ return this.auth_type; }-*/;
+  private final native JsArray<AccountFieldNameInfo> _editableAccountFields()
+  /*-{ return this.editable_account_fields; }-*/;
+
+  protected AuthInfo() {
+  }
+
+  private static class AccountFieldNameInfo extends JavaScriptObject {
+    final Account.FieldName get() {
+      return Account.FieldName.valueOf(getRaw());
+    }
+
+    private final native String getRaw() /*-{ return this; }-*/;
+
+    protected AccountFieldNameInfo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
index 5dedaf0..5d79390 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ConfigServerApi.java
@@ -37,4 +37,8 @@
   public static void defaultPreferences(AsyncCallback<Preferences> cb) {
     new RestApi("/config/server/preferences").get(cb);
   }
+
+  public static void serverInfo(AsyncCallback<ServerInfo> cb) {
+    new RestApi("/config/server/info").get(cb);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/DownloadInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/DownloadInfo.java
new file mode 100644
index 0000000..e97d472
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/DownloadInfo.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.config;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gwt.core.client.JavaScriptObject;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class DownloadInfo extends JavaScriptObject {
+  public final Set<String> schemes() {
+    return Natives.keys(_schemes());
+  }
+  public final native DownloadSchemeInfo scheme(String n) /*-{ return this.schemes[n]; }-*/;
+  private final native NativeMap<DownloadSchemeInfo> _schemes() /*-{ return this.schemes; }-*/;
+
+  protected DownloadInfo() {
+  }
+
+  public static class DownloadSchemeInfo extends JavaScriptObject {
+    public final Set<String> commandNames() {
+      return Natives.keys(_commands());
+    }
+
+    public final Set<DownloadCommandInfo> commands(String project) {
+      Set<DownloadCommandInfo> commands = new HashSet<>();
+      for (String commandName : commandNames()) {
+        commands.add(new DownloadCommandInfo(commandName, command(commandName,
+            project)));
+      }
+      return commands;
+    }
+
+    public final String command(String commandName, String project) {
+      return command(commandName).replaceAll("\\$\\{project\\}", project);
+    }
+
+    public final String getUrl(String project) {
+      return url().replaceAll("\\$\\{project\\}", project);
+    }
+
+    public final native String name() /*-{ return this.name; }-*/;
+    public final native String url() /*-{ return this.url; }-*/;
+    public final native boolean isAuthRequired() /*-{ return this.is_auth_required || false; }-*/;
+    public final native boolean isAuthSupported() /*-{ return this.is_auth_supported || false; }-*/;
+    public final native String command(String n) /*-{ return this.commands[n]; }-*/;
+    private final native NativeMap<NativeString> _commands() /*-{ return this.commands; }-*/;
+
+    protected DownloadSchemeInfo() {
+    }
+  }
+
+  public static class DownloadCommandInfo {
+    private final String name;
+    private final String command;
+
+    DownloadCommandInfo(String name, String command) {
+      this.name = name;
+      this.command = command;
+    }
+
+    public String name() {
+      return name;
+    }
+
+    public String command() {
+      return command;
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/GerritInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/GerritInfo.java
new file mode 100644
index 0000000..33036ad
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/GerritInfo.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.config;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class GerritInfo extends JavaScriptObject {
+  public final Project.NameKey allProjectsNameKey() {
+    return new Project.NameKey(allProjects());
+  }
+
+  public final boolean isAllProjects(Project.NameKey p) {
+    return allProjectsNameKey().equals(p);
+  }
+
+  public final Project.NameKey allUsersNameKey() {
+    return new Project.NameKey(allUsers());
+  }
+
+  public final boolean isAllUsers(Project.NameKey p) {
+    return allUsersNameKey().equals(p);
+  }
+
+  public final native String allProjects() /*-{ return this.all_projects; }-*/;
+  public final native String allUsers() /*-{ return this.all_users; }-*/;
+
+  protected GerritInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java
new file mode 100644
index 0000000..a1e8dbc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/config/ServerInfo.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.config;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ServerInfo extends JavaScriptObject {
+  public final native AuthInfo auth() /*-{ return this.auth; }-*/;
+  public final native ContactStoreInfo contactStore() /*-{ return this.contact_store; }-*/;
+  public final native DownloadInfo download() /*-{ return this.download; }-*/;
+  public final native GerritInfo gerrit() /*-{ return this.gerrit; }-*/;
+
+  public final boolean hasContactStore() {
+    return contactStore() != null;
+  }
+
+  protected ServerInfo() {
+  }
+
+  public static class ContactStoreInfo extends JavaScriptObject {
+    public final native String url() /*-{ return this.url; }-*/;
+
+    protected ContactStoreInfo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
index 4420940..8ff11e8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
@@ -124,7 +124,7 @@
     padding = new ArrayList<>();
     paddingDivs = new ArrayList<>();
 
-    String diffColor = diff.meta_a() == null || diff.meta_b() == null
+    String diffColor = diff.metaA() == null || diff.metaB() == null
         ? DiffTable.style.intralineBg()
         : DiffTable.style.diff();
 
@@ -175,8 +175,8 @@
 
     colorLines(cmA, color, startA, aLen);
     colorLines(cmB, color, startB, bLen);
-    markEdit(cmA, startA, a, region.edit_a());
-    markEdit(cmB, startB, b, region.edit_b());
+    markEdit(cmA, startA, a, region.editA());
+    markEdit(cmB, startB, b, region.editB());
     addPadding(cmA, startA + aLen - 1, bLen - aLen);
     addPadding(cmB, startB + bLen - 1, aLen - bLen);
     addGutterTag(region, startA, startB);
@@ -317,7 +317,7 @@
 
         DiffChunkInfo target = chunks.get(res);
         CodeMirror targetCm = host.getCmFromSide(target.getSide());
-        targetCm.setCursor(Pos.create(target.getStart()));
+        targetCm.setCursor(Pos.create(target.getStart(), 0));
         targetCm.focus();
         targetCm.scrollToY(
             targetCm.heightAtLine(target.getStart(), "local") -
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
index 7dc6c24b9..4e1a3e1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -218,8 +218,8 @@
         info,
         expandAll);
 
-    if (info.in_reply_to() != null) {
-      PublishedBox r = published.get(info.in_reply_to());
+    if (info.inReplyTo() != null) {
+      PublishedBox r = published.get(info.inReplyTo());
       if (r != null) {
         r.setReplyBox(box);
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
index a38a6ca..9f46e9b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentRange.java
@@ -38,10 +38,10 @@
         to.line() + 1, to.ch());
   }
 
-  public final native int start_line() /*-{ return this.start_line; }-*/;
-  public final native int start_character() /*-{ return this.start_character; }-*/;
-  public final native int end_line() /*-{ return this.end_line; }-*/;
-  public final native int end_character() /*-{ return this.end_character; }-*/;
+  public final native int startLine() /*-{ return this.start_line; }-*/;
+  public final native int startCharacter() /*-{ return this.start_character; }-*/;
+  public final native int endLine() /*-{ return this.end_line; }-*/;
+  public final native int endCharacter() /*-{ return this.end_character; }-*/;
 
   private final native void set(int sl, int sc, int el, int ec) /*-{
     this.start_line = sl;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
index 7140e07..f7f4528 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -30,24 +30,24 @@
   public static final String GITLINK = "x-git/gitlink";
   public static final String SYMLINK = "x-git/symlink";
 
-  public final native FileMeta meta_a() /*-{ return this.meta_a; }-*/;
-  public final native FileMeta meta_b() /*-{ return this.meta_b; }-*/;
-  public final native JsArrayString diff_header() /*-{ return this.diff_header; }-*/;
+  public final native FileMeta metaA() /*-{ return this.meta_a; }-*/;
+  public final native FileMeta metaB() /*-{ return this.meta_b; }-*/;
+  public final native JsArrayString diffHeader() /*-{ return this.diff_header; }-*/;
   public final native JsArray<Region> content() /*-{ return this.content; }-*/;
-  public final native JsArray<DiffWebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+  public final native JsArray<DiffWebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
   public final native boolean binary() /*-{ return this.binary || false; }-*/;
 
-  public final List<WebLinkInfo> side_by_side_web_links() {
+  public final List<WebLinkInfo> sideBySideWebLinks() {
     return filterWebLinks(DiffView.SIDE_BY_SIDE);
   }
 
-  public final List<WebLinkInfo> unified_web_links() {
+  public final List<WebLinkInfo> unifiedWebLinks() {
     return filterWebLinks(DiffView.UNIFIED_DIFF);
   }
 
   private final List<WebLinkInfo> filterWebLinks(DiffView diffView) {
     List<WebLinkInfo> filteredDiffWebLinks = new LinkedList<>();
-    List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(web_links());
+    List<DiffWebLinkInfo> allDiffWebLinks = Natives.asList(webLinks());
     if (allDiffWebLinks != null) {
       for (DiffWebLinkInfo webLink : allDiffWebLinks) {
         if (diffView == DiffView.SIDE_BY_SIDE
@@ -63,22 +63,22 @@
     return filteredDiffWebLinks;
   }
 
-  public final ChangeType change_type() {
-    return ChangeType.valueOf(change_typeRaw());
+  public final ChangeType changeType() {
+    return ChangeType.valueOf(changeTypeRaw());
   }
-  private final native String change_typeRaw()
+  private final native String changeTypeRaw()
   /*-{ return this.change_type }-*/;
 
-  public final IntraLineStatus intraline_status() {
-    String s = intraline_statusRaw();
+  public final IntraLineStatus intralineStatus() {
+    String s = intralineStatusRaw();
     return s != null
         ? IntraLineStatus.valueOf(s)
         : IntraLineStatus.OFF;
   }
-  private final native String intraline_statusRaw()
+  private final native String intralineStatusRaw()
   /*-{ return this.intraline_status }-*/;
 
-  public final boolean has_skip() {
+  public final boolean hasSkip() {
     JsArray<Region> c = content();
     for (int i = 0; i < c.length(); i++) {
       if (c.get(i).skip() != 0) {
@@ -88,7 +88,7 @@
     return false;
   }
 
-  public final String text_a() {
+  public final String textA() {
     StringBuilder s = new StringBuilder();
     JsArray<Region> c = content();
     for (int i = 0; i < c.length(); i++) {
@@ -103,7 +103,7 @@
     return s.toString();
   }
 
-  public final String text_b() {
+  public final String textB() {
     StringBuilder s = new StringBuilder();
     JsArray<Region> c = content();
     for (int i = 0; i < c.length(); i++) {
@@ -133,9 +133,9 @@
 
   public static class FileMeta extends JavaScriptObject {
     public final native String name() /*-{ return this.name; }-*/;
-    public final native String content_type() /*-{ return this.content_type; }-*/;
+    public final native String contentType() /*-{ return this.content_type; }-*/;
     public final native int lines() /*-{ return this.lines || 0 }-*/;
-    public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+    public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
     protected FileMeta() {
     }
@@ -148,8 +148,8 @@
     public final native int skip() /*-{ return this.skip || 0; }-*/;
     public final native boolean common() /*-{ return this.common || false; }-*/;
 
-    public final native JsArray<Span> edit_a() /*-{ return this.edit_a }-*/;
-    public final native JsArray<Span> edit_b() /*-{ return this.edit_b }-*/;
+    public final native JsArray<Span> editA() /*-{ return this.edit_a }-*/;
+    public final native JsArray<Span> editB() /*-{ return this.edit_b }-*/;
 
     protected Region() {
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
index 243ce4d..b188c59 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java
@@ -151,13 +151,13 @@
 
   void set(DiffPreferences prefs, JsArray<RevisionInfo> list, DiffInfo info,
       boolean editExists, int currentPatchSet, boolean open, boolean binary) {
-    this.changeType = info.change_type();
-    patchSetSelectBoxA.setUpPatchSetNav(list, info.meta_a(), editExists,
+    this.changeType = info.changeType();
+    patchSetSelectBoxA.setUpPatchSetNav(list, info.metaA(), editExists,
         currentPatchSet, open, binary);
-    patchSetSelectBoxB.setUpPatchSetNav(list, info.meta_b(), editExists,
+    patchSetSelectBoxB.setUpPatchSetNav(list, info.metaB(), editExists,
         currentPatchSet, open, binary);
 
-    JsArrayString hdr = info.diff_header();
+    JsArrayString hdr = info.diffHeader();
     if (hdr != null) {
       StringBuilder b = new StringBuilder();
       for (int i = 1; i < hdr.length(); i++) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
index 4d8c798..99ecc9b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml
@@ -17,12 +17,12 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:style gss="false" type='com.google.gerrit.client.diff.DiffTable.DiffTableStyle'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.DiffTable.DiffTableStyle'>
     @external .CodeMirror, .CodeMirror-selectedtext;
     @external .CodeMirror-linenumber;
     @external .CodeMirror-overlayscroll-vertical, .CodeMirror-scroll;
     @external .CodeMirror-dialog-bottom;
-    @external .cm-animate-fat-cursor, .CodeMirror-cursor;
+    @external .CodeMirror-cursor;
 
     .fullscreen {
       background-color: #f7f7f7;
@@ -116,18 +116,7 @@
       cursor: pointer;
     }
     .difftable .CodeMirror div.CodeMirror-cursor {
-      background: transparent;
-      text-decoration: underline;
-      border: none;
-      z-index: 2;
-    }
-    .difftable .cm-animate-fat-cursor {
-      text-decoration: underline;
-      border: none;
-      animation: none;
-      -webkit-animation: none;
-      -moz-animation: none;
-      -o-animation: none;
+      border-left: 2px solid black;
     }
     .difftable .CodeMirror-dialog-bottom {
       border-top: 0;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
index d7b528c..21b2f50 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.java
@@ -244,7 +244,7 @@
   }
 
   private void restoreSelection() {
-    if (getFromTo() != null && comment.in_reply_to() == null) {
+    if (getFromTo() != null && comment.inReplyTo() == null) {
       getCm().setSelection(getFromTo().from(), getFromTo().to());
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
index 4d05381..a363c06 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DraftBox.ui.xml
@@ -20,7 +20,7 @@
     xmlns:e='urn:import:com.google.gwtexpui.globalkey.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .draft {
       width: 45px;
       text-align: center;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
index c4459b6..77b28d4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/FileInfo.java
@@ -25,9 +25,9 @@
 
 public class FileInfo extends JavaScriptObject {
   public final native String path() /*-{ return this.path; }-*/;
-  public final native String old_path() /*-{ return this.old_path; }-*/;
-  public final native int lines_inserted() /*-{ return this.lines_inserted || 0; }-*/;
-  public final native int lines_deleted() /*-{ return this.lines_deleted || 0; }-*/;
+  public final native String oldPath() /*-{ return this.old_path; }-*/;
+  public final native int linesInserted() /*-{ return this.lines_inserted || 0; }-*/;
+  public final native int linesDeleted() /*-{ return this.lines_deleted || 0; }-*/;
   public final native boolean binary() /*-{ return this.binary || false; }-*/;
   public final native String status() /*-{ return this.status; }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index 2c551c0..42ae61d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -67,7 +67,7 @@
   }
 
   private static enum ReviewedState {
-    AUTO_REVIEW, LOADED;
+    AUTO_REVIEW, LOADED
   }
 
   @UiField CheckBox reviewed;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
index bd9b1b6..f13c9a3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.ui.xml
@@ -20,7 +20,7 @@
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
   <ui:with field='ico' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style gss="false">
+  <ui:style gss='false'>
   .header {
     position: relative;
     height: 16px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml
index 5c84c93..6a18c4d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.ui.xml
@@ -18,7 +18,7 @@
     xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .bubble {
       z-index: 150;
       white-space: nowrap;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
index bbc5830..e597398 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
@@ -116,7 +116,7 @@
         linkPanel.add(createEditIcon());
       }
     }
-    List<WebLinkInfo> webLinks = Natives.asList(meta.web_links());
+    List<WebLinkInfo> webLinks = Natives.asList(meta.webLinks());
     if (webLinks != null) {
       for (WebLinkInfo webLink : webLinks) {
         linkPanel.add(webLink.toAnchor());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
index cda728f..6e526ec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.ui.xml
@@ -20,7 +20,7 @@
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
   <ui:with field='patchConstants'
       type='com.google.gerrit.client.patches.PatchConstants'/>
-  <ui:style gss="false" type='com.google.gerrit.client.diff.PatchSetSelectBox.BoxStyle'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.PatchSetSelectBox.BoxStyle'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
     .table {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
index edd554b..e011091 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.ui.xml
@@ -17,7 +17,7 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:x='urn:import:com.google.gerrit.client.ui'>
-  <ui:style gss="false" type='com.google.gerrit.client.diff.PreferencesBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.PreferencesBox.Style'>
     @external .gwt-TextBox;
     @external .gwt-ToggleButton .html-face;
     @external .gwt-ToggleButton-up;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
index b1cde7b..46b76ca 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.ui.xml
@@ -19,7 +19,7 @@
     xmlns:c='urn:import:com.google.gerrit.client'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
-  <ui:style gss="false" type='com.google.gerrit.client.diff.PublishedBox.Style'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.PublishedBox.Style'>
     .avatar {
       position: absolute;
       width: 26px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
index d06b59e..a8b6065 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
@@ -238,11 +238,11 @@
         changeStatus = info.status();
         info.revisions().copyKeysIntoChildren("name");
         if (edit != null) {
-          edit.set_name(edit.commit().commit());
-          info.set_edit(edit);
+          edit.setName(edit.commit().commit());
+          info.setEdit(edit);
           info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
         }
-        int currentPatchSet = info.revision(info.current_revision())._number();
+        int currentPatchSet = info.revision(info.currentRevision())._number();
         JsArray<RevisionInfo> list = info.revisions().values();
         RevisionInfo.sortRevisionInfoByNumber(list);
         diffTable.set(prefs, list, diff, edit != null, currentPatchSet,
@@ -570,8 +570,8 @@
       diffTable.addStyleName(DiffTable.style.showLineNumbers());
     }
 
-    cmA = newCM(diff.meta_a(), diff.text_a(), diffTable.cmA);
-    cmB = newCM(diff.meta_b(), diff.text_b(), diffTable.cmB);
+    cmA = newCM(diff.metaA(), diff.textA(), diffTable.cmA);
+    cmB = newCM(diff.metaB(), diff.textB(), diffTable.cmB);
 
     cmA.extras().side(DisplaySide.A);
     cmB.extras().side(DisplaySide.B);
@@ -606,7 +606,7 @@
             chunkManager.getLineMapper());
 
     prefsAction = new PreferencesAction(this, prefs);
-    header.init(prefsAction, getLinks(), diff.side_by_side_web_links());
+    header.init(prefsAction, getLinks(), diff.sideBySideWebLinks());
     scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader());
 
     if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
@@ -654,7 +654,7 @@
   }
 
   DiffInfo.IntraLineStatus getIntraLineStatus() {
-    return diff.intraline_status();
+    return diff.intralineStatus();
   }
 
   boolean canEnableRenderEntireFile(DiffPreferences prefs) {
@@ -663,7 +663,7 @@
   }
 
   String getContentType() {
-    return getContentType(diff.meta_b());
+    return getContentType(diff.metaB());
   }
 
   void setThemeStyles(boolean d) {
@@ -716,8 +716,8 @@
         @Override
         public void onSuccess(Void result) {
           if (prefs.syntaxHighlighting()) {
-            cmA.setOption("mode", getContentType(diff.meta_a()));
-            cmB.setOption("mode", getContentType(diff.meta_b()));
+            cmA.setOption("mode", getContentType(diff.metaA()));
+            cmB.setOption("mode", getContentType(diff.metaB()));
           }
         }
 
@@ -923,7 +923,7 @@
     int offset = 6;
 
     // Adjust for merge commits, which have two parent lines
-    if (diff.text_b().startsWith("Merge")) {
+    if (diff.textB().startsWith("Merge")) {
       offset += 1;
     }
 
@@ -983,8 +983,8 @@
 
   private String getContentType(DiffInfo.FileMeta meta) {
     if (prefs.syntaxHighlighting() && meta != null
-        && meta.content_type() != null) {
-     ModeInfo m = ModeInfo.findMode(meta.content_type(), path);
+        && meta.contentType() != null) {
+     ModeInfo m = ModeInfo.findMode(meta.contentType(), path);
      return m != null ? m.mime() : null;
    }
    return null;
@@ -992,8 +992,8 @@
 
   private void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) {
     new ModeInjector()
-      .add(getContentType(diffInfo.meta_a()))
-      .add(getContentType(diffInfo.meta_b()))
+      .add(getContentType(diffInfo.metaA()))
+      .add(getContentType(diffInfo.metaB()))
       .inject(cb);
   }
 
@@ -1043,8 +1043,8 @@
           @Override
           public void onSuccess(DiffInfo info) {
             new ModeInjector()
-              .add(getContentType(info.meta_a()))
-              .add(getContentType(info.meta_b()))
+              .add(getContentType(info.metaA()))
+              .add(getContentType(info.metaB()))
               .inject(CallbackGroup.<Void> emptyCallback());
           }
 
@@ -1085,8 +1085,8 @@
   }
 
   private static FileSize bucketFileSize(DiffInfo diff) {
-    FileMeta a = diff.meta_a();
-    FileMeta b = diff.meta_b();
+    FileMeta a = diff.metaA();
+    FileMeta b = diff.metaB();
     FileSize[] sizes = FileSize.values();
     for (int i = sizes.length - 1; 0 <= i; i--) {
       FileSize s = sizes[i];
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
index 4f1a3c5..a4c2eb9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml
@@ -17,7 +17,7 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'
     xmlns:d='urn:import:com.google.gerrit.client.diff'>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .sbs {
       margin-left: -5px;
       margin-right: -5px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
index 4d7b133..bf3c425 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml
@@ -16,7 +16,7 @@
 -->
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss="false" type='com.google.gerrit.client.diff.SkipBar.SkipBarStyle'>
+  <ui:style gss='false' type='com.google.gerrit.client.diff.SkipBar.SkipBarStyle'>
     .skipBar {
       background-color: #def;
       height: 1.3em;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
index cea9106..d4237f9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
@@ -15,8 +15,6 @@
 package com.google.gerrit.client.download;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -25,89 +23,15 @@
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 public abstract class DownloadCommandLink extends Anchor implements ClickHandler {
   public static class CopyableCommandLinkFactory {
     protected CopyableLabel copyLabel = null;
     protected Widget widget;
 
-    public class CheckoutCommandLink extends DownloadCommandLink {
-      public CheckoutCommandLink () {
-        super(DownloadCommand.CHECKOUT, "checkout");
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-        copyLabel.setText("git fetch " + link.getUrlData()
-            + " && git checkout FETCH_HEAD");
-      }
-    }
-
-    public class PullCommandLink extends DownloadCommandLink {
-      public PullCommandLink() {
-        super(DownloadCommand.PULL, "pull");
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-        copyLabel.setText("git pull " + link.getUrlData());
-      }
-    }
-
-    public class CherryPickCommandLink extends DownloadCommandLink {
-      public CherryPickCommandLink() {
-        super(DownloadCommand.CHERRY_PICK, "cherry-pick");
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-        copyLabel.setText("git fetch " + link.getUrlData()
-            + " && git cherry-pick FETCH_HEAD");
-      }
-    }
-
-    public class FormatPatchCommandLink extends DownloadCommandLink {
-      public FormatPatchCommandLink() {
-        super(DownloadCommand.FORMAT_PATCH, "patch");
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(true);
-        copyLabel.setText("git fetch " + link.getUrlData()
-            + " && git format-patch -1 --stdout FETCH_HEAD");
-      }
-    }
-
-    public class RepoCommandLink extends DownloadCommandLink {
-      String projectName;
-      String ref;
-      public RepoCommandLink(String project, String ref) {
-        super(DownloadCommand.REPO_DOWNLOAD, "repo download");
-        this.projectName = project;
-        this.ref = ref;
-      }
-
-      @Override
-      protected void setCurrentUrl(DownloadUrlLink link) {
-        widget.setVisible(false);
-        final StringBuilder r = new StringBuilder();
-        r.append("repo download ");
-        r.append(projectName);
-        r.append(" ");
-        r.append(ref);
-        copyLabel.setText(r.toString());
-      }
-    }
-
     public class CloneCommandLink extends DownloadCommandLink {
       public CloneCommandLink() {
-        super(DownloadCommand.CHECKOUT, "clone");
+        super("clone");
       }
 
       @Override
@@ -121,7 +45,7 @@
       private final Project.NameKey project;
 
       public CloneWithCommitMsgHookCommandLink(Project.NameKey project) {
-        super(DownloadCommand.CHECKOUT, "clone with commit-msg hook");
+        super("clone with commit-msg hook");
         this.project = project;
       }
 
@@ -175,12 +99,8 @@
     }
   }
 
-  final DownloadCommand cmdType;
-
-  public DownloadCommandLink(DownloadCommand cmdType,
-      String text) {
+  public DownloadCommandLink(String text) {
     super(text);
-    this.cmdType = cmdType;
     setStyleName(Gerrit.RESOURCES.css().downloadLink());
     Roles.getTabRole().set(getElement());
     addClickHandler(this);
@@ -192,28 +112,6 @@
     event.stopPropagation();
 
     select();
-
-    if (Gerrit.isSignedIn()) {
-      // If the user is signed-in, remember this choice for future panels.
-      //
-      AccountGeneralPreferences pref =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      pref.setDownloadCommand(cmdType);
-      com.google.gerrit.client.account.Util.ACCOUNT_SVC.changePreferences(pref,
-          new AsyncCallback<VoidResult>() {
-            @Override
-            public void onFailure(Throwable caught) {
-            }
-
-            @Override
-            public void onSuccess(VoidResult result) {
-            }
-          });
-    }
-  }
-
-  public DownloadCommand getCmdType() {
-    return cmdType;
   }
 
   void select() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
index d17d6c2..5b7d015 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
@@ -15,8 +15,6 @@
 package com.google.gerrit.client.download;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
@@ -34,7 +32,7 @@
     return getWidgetCount() == 0;
   }
 
-  public void select(AccountGeneralPreferences.DownloadCommand cmdType) {
+  public void select() {
     DownloadCommandLink first = null;
 
     for (Widget w : this) {
@@ -43,10 +41,6 @@
         if (first == null) {
           first = d;
         }
-        if (d.cmdType == cmdType) {
-          d.select();
-          return;
-        }
       }
     }
 
@@ -70,9 +64,6 @@
   private void update() {
     if (currentCommand != null && currentUrl != null) {
       currentCommand.setCurrentUrl(currentUrl);
-    } else if (currentCommand != null &&
-        currentCommand.getCmdType().equals(DownloadCommand.REPO_DOWNLOAD)) {
-      currentCommand.setCurrentUrl(null);
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
index 350dbed..19c65a9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
@@ -16,32 +16,24 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
-import java.util.Set;
-
 public abstract class DownloadPanel extends FlowPanel {
   protected String projectName;
 
-  protected Set<DownloadScheme> allowedSchemes =
-      Gerrit.getConfig().getDownloadSchemes();
-  protected Set<DownloadCommand> allowedCommands =
-      Gerrit.getConfig().getDownloadCommands();
   protected DownloadCommandLink.CopyableCommandLinkFactory cmdLinkfactory;
 
   protected DownloadCommandPanel commands = new DownloadCommandPanel();
   protected DownloadUrlPanel urls = new DownloadUrlPanel(commands);
   protected CopyableLabel copyLabel = new CopyableLabel("");
 
-  public DownloadPanel(String project, String ref, boolean allowAnonymous) {
+  public DownloadPanel(String project, boolean allowAnonymous) {
     this.projectName = project;
 
     copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadLinkCopyLabel());
-    urls.add(DownloadUrlLink.createDownloadUrlLinks(project, ref, allowAnonymous));
+    urls.add(DownloadUrlLink.createDownloadUrlLinks(project, allowAnonymous));
     cmdLinkfactory = new DownloadCommandLink.CopyableCommandLinkFactory(
         copyLabel, urls);
 
@@ -58,7 +50,7 @@
         pref = new AccountGeneralPreferences();
         pref.resetToDefaults();
       }
-      commands.select(pref.getDownloadCommand());
+      commands.select();
       urls.select(pref.getDownloadUrl());
 
       FlowPanel p = new FlowPanel();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
index ce5c060..275b918 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
-import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -33,28 +32,9 @@
 import java.util.Set;
 
 public class DownloadUrlLink extends Anchor implements ClickHandler {
-  public static class DownloadRefUrlLink extends DownloadUrlLink {
-    protected String projectName;
-    protected String ref;
-
-    protected DownloadRefUrlLink(DownloadScheme urlType,
-        String text, String project, String ref) {
-      super(urlType, text);
-      this.projectName = project;
-      this.ref = ref;
-    }
-
-    protected void appendRef(StringBuilder r) {
-      if (ref != null) {
-        r.append(" ");
-        r.append(ref);
-      }
-    }
-  }
-
-  public static class AnonGitLink extends DownloadRefUrlLink {
-    public AnonGitLink(String project, String ref) {
-      super(DownloadScheme.ANON_GIT, Util.M.anonymousDownload("Git"), project, ref);
+  public static class AnonGitLink extends DownloadUrlLink {
+    public AnonGitLink(String project) {
+      super(DownloadScheme.ANON_GIT, Util.M.anonymousDownload("Git"), project);
     }
 
     @Override
@@ -62,14 +42,13 @@
       StringBuilder r = new StringBuilder();
       r.append(Gerrit.getConfig().getGitDaemonUrl());
       r.append(projectName);
-      appendRef(r);
       return r.toString();
     }
   }
 
-  public static class AnonHttpLink extends DownloadRefUrlLink {
-    public AnonHttpLink(String project, String ref) {
-      super(DownloadScheme.ANON_HTTP, Util.M.anonymousDownload("HTTP"), project, ref);
+  public static class AnonHttpLink extends DownloadUrlLink {
+    public AnonHttpLink(String project) {
+      super(DownloadScheme.ANON_HTTP, Util.M.anonymousDownload("HTTP"), project);
     }
 
     @Override
@@ -81,14 +60,13 @@
         r.append(hostPageUrl);
       }
       r.append(projectName);
-      appendRef(r);
       return r.toString();
     }
   }
 
-  public static class SshLink extends DownloadRefUrlLink {
-    public SshLink(String project, String ref) {
-      super(DownloadScheme.SSH, "SSH", project, ref);
+  public static class SshLink extends DownloadUrlLink {
+    public SshLink(String project) {
+      super(DownloadScheme.SSH, "SSH", project);
     }
 
     @Override
@@ -107,16 +85,15 @@
       r.append(sshAddr);
       r.append("/");
       r.append(projectName);
-      appendRef(r);
       return r.toString();
     }
   }
 
-  public static class HttpLink extends DownloadRefUrlLink {
+  public static class HttpLink extends DownloadUrlLink {
     protected boolean anonymous;
 
-    public HttpLink(String project, String ref, boolean anonymous) {
-      super(DownloadScheme.HTTP, "HTTP", project, ref);
+    public HttpLink(String project, boolean anonymous) {
+      super(DownloadScheme.HTTP, "HTTP", project);
       this.anonymous = anonymous;
     }
 
@@ -145,46 +122,41 @@
         r.append(base.substring(s));
       }
       r.append(projectName);
-      appendRef(r);
       return r.toString();
     }
   }
 
   public static boolean siteReliesOnHttp() {
     return Gerrit.getConfig().getGitHttpUrl() != null
-        && Gerrit.getConfig().getAuthType() == AuthType.CUSTOM_EXTENSION
+        && Gerrit.info().auth().isCustomExtension()
         && !Gerrit.getConfig().siteHasUsernames();
   }
 
   public static List<DownloadUrlLink> createDownloadUrlLinks(String project,
-      String ref, boolean allowAnonymous) {
+      boolean allowAnonymous) {
     List<DownloadUrlLink> urls = new ArrayList<>();
-    Set<DownloadScheme> allowedSchemes = Gerrit.getConfig().getDownloadSchemes();
+    Set<String> allowedSchemes = Gerrit.info().download().schemes();
 
     if (allowAnonymous
         && Gerrit.getConfig().getGitDaemonUrl() != null
-        && (allowedSchemes.contains(DownloadScheme.ANON_GIT) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.AnonGitLink(project, ref));
+        && allowedSchemes.contains("git")) {
+      urls.add(new DownloadUrlLink.AnonGitLink(project));
     }
 
     if (allowAnonymous
-        && (allowedSchemes.contains(DownloadScheme.ANON_HTTP) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.AnonHttpLink(project, ref));
+        && allowedSchemes.contains("anonymous http")) {
+      urls.add(new DownloadUrlLink.AnonHttpLink(project));
     }
 
     if (Gerrit.getConfig().getSshdAddress() != null
         && hasUserName()
-        && (allowedSchemes.contains(DownloadScheme.SSH) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.SshLink(project, ref));
+        && allowedSchemes.contains("ssh")) {
+      urls.add(new DownloadUrlLink.SshLink(project));
     }
 
     if ((hasUserName() || siteReliesOnHttp())
-        && (allowedSchemes.contains(DownloadScheme.HTTP)
-            || allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      urls.add(new DownloadUrlLink.HttpLink(project, ref, allowAnonymous));
+        && allowedSchemes.contains("http")) {
+      urls.add(new DownloadUrlLink.HttpLink(project, allowAnonymous));
     }
     return urls;
   }
@@ -196,21 +168,11 @@
   }
 
   protected DownloadScheme urlType;
+  protected String projectName;
   protected String urlData;
   protected String hostPageUrl = GWT.getHostPageBaseURL();
 
-  public DownloadUrlLink(DownloadScheme urlType, String text, String urlData) {
-    this(text);
-    this.urlType = urlType;
-    this.urlData = urlData;
-  }
-
-  public DownloadUrlLink(DownloadScheme urlType, String text) {
-    this(text);
-    this.urlType = urlType;
-  }
-
-  public DownloadUrlLink(String text) {
+  public DownloadUrlLink(DownloadScheme urlType, String text, String project) {
     super(text);
     setStyleName(Gerrit.RESOURCES.css().downloadLink());
     Roles.getTabRole().set(getElement());
@@ -219,6 +181,8 @@
     if (!hostPageUrl.endsWith("/")) {
       hostPageUrl += "/";
     }
+    this.urlType = urlType;
+    this.projectName = project;
   }
 
   public String getUrlData() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
index c833c5d..dda5fc2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditFileInfo.java
@@ -19,7 +19,7 @@
 import com.google.gwt.core.client.JsArray;
 
 public class EditFileInfo extends JavaScriptObject {
-  public final native JsArray<DiffWebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+  public final native JsArray<DiffWebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
   protected EditFileInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index dd36657..e463607 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -185,7 +185,7 @@
         .get(group1.add(new AsyncCallback<DiffInfo>() {
           @Override
           public void onSuccess(DiffInfo diffInfo) {
-            diffLinks = diffInfo.web_links();
+            diffLinks = diffInfo.webLinks();
           }
 
           @Override
@@ -377,7 +377,7 @@
     renderLinksToDiff();
 
     if (editInfo != null) {
-      renderLinks(Natives.asList(editInfo.web_links()));
+      renderLinks(Natives.asList(editInfo.webLinks()));
     } else if (diffLinks != null) {
       renderLinks(Natives.asList(diffLinks));
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
index 9458f50..f68d14a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.ui.xml
@@ -16,7 +16,7 @@
 -->
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     .header {
       position: relative;
       height: 16px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
index 0b178f2..e2b1112 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
@@ -125,7 +125,7 @@
     } else {
       MemberInput input = MemberInput.create();
       for (String member : members) {
-        input.add_member(member);
+        input.addMember(member);
       }
       members(group).post(input, cb);
     }
@@ -139,7 +139,7 @@
     } else {
       MemberInput in = MemberInput.create();
       for (Integer id : ids) {
-        in.add_member(id.toString());
+        in.addMember(id.toString());
       }
       group(group).view("members.delete").post(in, cb);
     }
@@ -172,7 +172,7 @@
     } else {
       IncludedGroupInput input = IncludedGroupInput.create();
       for (String includedGroup : includedGroups) {
-        input.add_group(includedGroup);
+        input.addGroup(includedGroup);
       }
       groups(group).post(input, cb);
     }
@@ -187,7 +187,7 @@
     } else {
       IncludedGroupInput in = IncludedGroupInput.create();
       for (AccountGroup.UUID g : ids) {
-        in.add_group(g.get());
+        in.addGroup(g.get());
       }
       group(group).view("groups.delete").post(in, cb);
     }
@@ -235,7 +235,7 @@
 
   private static class MemberInput extends JavaScriptObject {
     final native void init() /*-{ this.members = []; }-*/;
-    final native void add_member(String n) /*-{ this.members.push(n); }-*/;
+    final native void addMember(String n) /*-{ this.members.push(n); }-*/;
 
     static MemberInput create() {
       MemberInput m = (MemberInput) createObject();
@@ -249,7 +249,7 @@
 
   private static class IncludedGroupInput extends JavaScriptObject {
     final native void init() /*-{ this.groups = []; }-*/;
-    final native void add_group(String n) /*-{ this.groups.push(n); }-*/;
+    final native void addGroup(String n) /*-{ this.groups.push(n); }-*/;
 
     static IncludedGroupInput create() {
       IncludedGroupInput g = (IncludedGroupInput) createObject();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index 2538102..8642556 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -345,7 +345,7 @@
     if (c.getLine() > 0) {
       i.line(c.getLine());
     }
-    i.in_reply_to(c.getParentUuid());
+    i.inReplyTo(c.getParentUuid());
     i.message(c.getMessage());
     return i;
   }
@@ -359,7 +359,7 @@
             i.id()),
         i.line(),
         Gerrit.getUserAccount().getId(),
-        i.in_reply_to(),
+        i.inReplyTo(),
         i.updated());
     p.setMessage(i.message());
     p.setSide((short) (i.side() == Side.PARENT ? 0 : 1));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
index 37d3fb7..f1bf3de 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommitMessageBlock.ui.xml
@@ -20,7 +20,7 @@
 
 
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style gss="false">
+  <ui:style gss='false'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
     @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
     @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
index be82f66..5164302 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
@@ -22,7 +22,7 @@
   ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
   ui:generateLocales='default,en'
   >
-<ui:style gss="false">
+<ui:style gss='false'>
   @external .gwt-TextBox;
   @external .gwt-ListBox;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
index bfcf2f6..8977876 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
@@ -18,7 +18,7 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:style gss="false" type='com.google.gerrit.client.patches.PatchSetSelectBox.BoxStyle'>
+  <ui:style gss='false' type='com.google.gerrit.client.patches.PatchSetSelectBox.BoxStyle'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
     @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
index a5c1484..2479322 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
@@ -251,7 +251,7 @@
   }
 
   private List<WebLinkInfo> getWebLinks(DiffInfo diffInfo) {
-    return diffInfo.unified_web_links();
+    return diffInfo.unifiedWebLinks();
   }
 
   private String getSideBySideDiffUrl() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
index 6c1a841..0284aa3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/BranchInfo.java
@@ -17,22 +17,20 @@
 import com.google.gerrit.client.WebLinkInfo;
 import com.google.gerrit.client.actions.ActionInfo;
 import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.JsArray;
 
 public class BranchInfo extends JavaScriptObject {
   public final String getShortName() {
-    return ref().startsWith(Branch.R_HEADS)
-        ? ref().substring(Branch.R_HEADS.length())
-        : ref();
+    return RefNames.shortName(ref());
   }
 
   public final native String ref() /*-{ return this.ref; }-*/;
   public final native String revision() /*-{ return this.revision; }-*/;
   public final native boolean canDelete() /*-{ return this['can_delete'] ? true : false; }-*/;
   public final native NativeMap<ActionInfo> actions() /*-{ return this.actions }-*/;
-  public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+  public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
   protected BranchInfo() {
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index 7aa5be5..b91c5de 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -35,23 +35,23 @@
   public final native String description()
   /*-{ return this.description }-*/;
 
-  public final native InheritedBooleanInfo require_change_id()
+  public final native InheritedBooleanInfo requireChangeId()
   /*-{ return this.require_change_id; }-*/;
 
-  public final native InheritedBooleanInfo use_content_merge()
+  public final native InheritedBooleanInfo useContentMerge()
   /*-{ return this.use_content_merge; }-*/;
 
-  public final native InheritedBooleanInfo use_contributor_agreements()
+  public final native InheritedBooleanInfo useContributorAgreements()
   /*-{ return this.use_contributor_agreements; }-*/;
 
-  public final native InheritedBooleanInfo create_new_change_for_all_not_in_target()
+  public final native InheritedBooleanInfo createNewChangeForAllNotInTarget()
   /*-{ return this.create_new_change_for_all_not_in_target; }-*/;
 
-  public final native InheritedBooleanInfo use_signed_off_by()
+  public final native InheritedBooleanInfo useSignedOffBy()
   /*-{ return this.use_signed_off_by; }-*/;
 
-  public final SubmitType submit_type() {
-    return SubmitType.valueOf(submit_typeRaw());
+  public final SubmitType submitType() {
+    return SubmitType.valueOf(submitTypeRaw());
   }
 
   public final native NativeMap<NativeMap<ConfigParameterInfo>> pluginConfig()
@@ -63,7 +63,7 @@
   public final native NativeMap<ActionInfo> actions()
   /*-{ return this.actions; }-*/;
 
-  private final native String submit_typeRaw()
+  private final native String submitTypeRaw()
   /*-{ return this.submit_type }-*/;
 
   public final ProjectState state() {
@@ -75,7 +75,7 @@
   private final native String stateRaw()
   /*-{ return this.state }-*/;
 
-  public final native MaxObjectSizeLimitInfo max_object_size_limit()
+  public final native MaxObjectSizeLimitInfo maxObjectSizeLimit()
   /*-{ return this.max_object_size_limit; }-*/;
 
   private final native NativeMap<CommentLinkInfo> commentlinks0()
@@ -131,13 +131,13 @@
     public final native boolean value()
     /*-{ return this.value ? true : false; }-*/;
 
-    public final native boolean inherited_value()
+    public final native boolean inheritedValue()
     /*-{ return this.inherited_value ? true : false; }-*/;
 
-    public final InheritableBoolean configured_value() {
-      return InheritableBoolean.valueOf(configured_valueRaw());
+    public final InheritableBoolean configuredValue() {
+      return InheritableBoolean.valueOf(configuredValueRaw());
     }
-    private final native String configured_valueRaw()
+    private final native String configuredValueRaw()
     /*-{ return this.configured_value }-*/;
 
     public final void setConfiguredValue(InheritableBoolean v) {
@@ -152,8 +152,8 @@
 
   public static class MaxObjectSizeLimitInfo extends JavaScriptObject {
     public final native String value() /*-{ return this.value; }-*/;
-    public final native String inherited_value() /*-{ return this.inherited_value; }-*/;
-    public final native String configured_value() /*-{ return this.configured_value }-*/;
+    public final native String inheritedValue() /*-{ return this.inherited_value; }-*/;
+    public final native String configuredValue() /*-{ return this.configured_value }-*/;
 
     protected MaxObjectSizeLimitInfo() {
     }
@@ -179,8 +179,8 @@
 
   public static class ConfigParameterValue extends JavaScriptObject {
     final native void init() /*-{ this.values = []; }-*/;
-    final native void add_value(String v) /*-{ this.values.push(v); }-*/;
-    final native void set_value(String v) /*-{ if(v)this.value = v; }-*/;
+    final native void addValue(String v) /*-{ this.values.push(v); }-*/;
+    final native void setValue(String v) /*-{ if(v)this.value = v; }-*/;
     public static ConfigParameterValue create() {
       ConfigParameterValue v = createObject().cast();
       return v;
@@ -189,13 +189,13 @@
     public final ConfigParameterValue values(String[] values) {
       init();
       for (String v : values) {
-        add_value(v);
+        addValue(v);
       }
       return this;
     }
 
     public final ConfigParameterValue value(String v) {
-      set_value(v);
+      setValue(v);
       return this;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
index c22b007..e000a97 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
@@ -61,7 +61,7 @@
   }
 
   public static void add(ChangeInfo info) {
-    instance.changeToProject.put(info.legacy_id().get(), info.project());
+    instance.changeToProject.put(info.legacyId().get(), info.project());
   }
 
   private final LinkedHashMap<String, Entry> cache;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index b121d07..d81dfe5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -84,7 +84,7 @@
     } else {
       DeleteBranchesInput d = DeleteBranchesInput.create();
       for (String ref : refs) {
-        d.add_branch(ref);
+        d.addBranch(ref);
       }
       project(name).view("branches:delete").post(d, cb);
     }
@@ -317,6 +317,6 @@
     }
 
     final native void init() /*-{ this.branches = []; }-*/;
-    final native void add_branch(String b) /*-{ this.branches.push(b); }-*/;
+    final native void addBranch(String b) /*-{ this.branches.push(b); }-*/;
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
index fe9872c..029e59b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
@@ -30,7 +30,7 @@
 
   public final native String name() /*-{ return this.name; }-*/;
   public final native String description() /*-{ return this.description; }-*/;
-  public final native JsArray<WebLinkInfo> web_links() /*-{ return this.web_links; }-*/;
+  public final native JsArray<WebLinkInfo> webLinks() /*-{ return this.web_links; }-*/;
 
   public final ProjectState state() {
     return ProjectState.valueOf(getStringState());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
index cdad972..2633e3b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLinkPanel.java
@@ -71,8 +71,8 @@
       return ai.email();
     } else if (ai.name() != null) {
       return ai.name();
-    } else if (ai._account_id() != 0) {
-      return "" + ai._account_id();
+    } else if (ai._accountId() != 0) {
+      return "" + ai._accountId();
     } else {
       return "";
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
index c9a0590..6af4b78 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 
 /** Link to the open changes of a project. */
 public class BranchLink extends InlineHyperlink {
@@ -61,10 +61,10 @@
       String branch, String topic) {
     String query = PageLinks.projectQuery(project, status);
 
-    if (branch.startsWith(Branch.R_REFS)) {
-      if (branch.startsWith(Branch.R_HEADS)) {
+    if (branch.startsWith(RefNames.REFS)) {
+      if (branch.startsWith(RefNames.REFS_HEADS)) {
         query += " " + PageLinks.op("branch", //
-            branch.substring(Branch.R_HEADS.length()));
+            branch.substring(RefNames.REFS_HEADS.length()));
       } else {
         query += " " + PageLinks.op("ref", branch);
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
index 6ff92e6..a2b4aa8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CreateChangeDialog.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -91,10 +90,7 @@
 
     @Override
     public String getDisplayString() {
-      if (branch.ref().startsWith(Branch.R_HEADS)) {
-        return branch.ref().substring(Branch.R_HEADS.length());
-      }
-      return branch.ref();
+      return branch.getShortName();
     }
 
     @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
index a2155d4..819a11f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
@@ -87,7 +87,7 @@
     // Resetting the "original text" on focus ensures that we are
     // up to date with non-user updates of the text (calls to
     // setText()...) and also up to date with user changes which
-    // occured after enabling "widget".
+    // occurred after enabling "widget".
     tb.addFocusHandler(new FocusHandler() {
         @Override
         public void onFocus(FocusEvent event) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
index 5f47d98..2744877 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RebaseDialog.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -29,6 +30,7 @@
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -51,10 +53,10 @@
         String query = request.getQuery().toLowerCase();
         LinkedList<ChangeSuggestion> suggestions = new LinkedList<>();
         for (final ChangeInfo ci : changes) {
-          if (changeId.equals(ci.legacy_id())) {
+          if (changeId.equals(ci.legacyId())) {
             continue;  // do not suggest current change
           }
-          String id = String.valueOf(ci.legacy_id().get());
+          String id = String.valueOf(ci.legacyId().get());
           if (id.contains(query) || ci.subject().toLowerCase().contains(query)) {
             suggestions.add(new ChangeSuggestion(ci));
             if (suggestions.size() >= 50) { // limit to 50 suggestions
@@ -76,8 +78,10 @@
       public void onClick(ClickEvent event) {
         boolean checked = ((CheckBox) event.getSource()).getValue();
         if (checked) {
-          ChangeList.next("project:" + project + " AND branch:" + branch
-              + " AND is:open NOT age:90d", 0, 1000,
+          ChangeList.query(
+              "project:" + project + " AND branch:" + branch
+                  + " AND is:open NOT age:90d",
+              Collections.<ListChangesOption> emptySet(),
               new GerritCallback<ChangeList>() {
                 @Override
                 public void onSuccess(ChangeList result) {
@@ -136,12 +140,12 @@
 
     @Override
     public String getDisplayString() {
-      return String.valueOf(change.legacy_id().get()) + ": " + change.subject();
+      return String.valueOf(change.legacyId().get()) + ": " + change.subject();
     }
 
     @Override
     public String getReplacementString() {
-      return String.valueOf(change.legacy_id().get());
+      return String.valueOf(change.legacyId().get());
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
index f4be9d4..639e5e7 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java
@@ -219,7 +219,7 @@
     if (lineAtHeight(height - 20) < line) {
       scrollToY(heightAtLine(line, "local") - 0.5 * height);
     }
-    setCursor(Pos.create(line));
+    setCursor(Pos.create(line, 0));
   }
 
   public final native ScrollInfo getScrollInfo() /*-{
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
index 2d69015..eac8510 100644
--- a/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
+++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/TextMarker.java
@@ -35,8 +35,8 @@
 
     public static FromTo create(CommentRange range) {
       return create(
-          Pos.create(range.start_line() - 1, range.start_character()),
-          Pos.create(range.end_line() - 1, range.end_character()));
+          Pos.create(range.startLine() - 1, range.startCharacter()),
+          Pos.create(range.endLine() - 1, range.endCharacter()));
     }
 
     public final native Pos from() /*-{ return this.from }-*/;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index 91fb6af..378c4d5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -52,8 +52,6 @@
  */
 @Singleton
 class ContainerAuthFilter implements Filter {
-  public static final String REALM_NAME = "Gerrit Code Review";
-
   private final DynamicItem<WebSession> session;
   private final AccountCache accountCache;
   private final Config config;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index 25c5321..eadc536 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -16,22 +16,18 @@
 
 import com.google.common.base.Function;
 import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.GitwebConfig;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.gerrit.server.change.GetArchive;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.contact.ContactStore;
-import com.google.gerrit.server.mail.EmailSender;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -40,8 +36,6 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.net.MalformedURLException;
-import java.util.HashSet;
-import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 import javax.servlet.ServletContext;
@@ -50,42 +44,32 @@
   private final Realm realm;
   private final Config cfg;
   private final AuthConfig authConfig;
-  private final DownloadConfig downloadConfig;
   private final GetArchive.AllowedFormats archiveFormats;
   private final GitWebConfig gitWebConfig;
-  private final AllProjectsName wildProject;
   private final SshInfo sshInfo;
 
-  private EmailSender emailSender;
-  private final ContactStore contactStore;
   private final ServletContext servletContext;
   private final String anonymousCowardName;
 
   @Inject
-  GerritConfigProvider(final Realm r, @GerritServerConfig final Config gsc,
-      final AuthConfig ac, final GitWebConfig gwc, final AllProjectsName wp,
-      final SshInfo si, final ContactStore cs,
-      final ServletContext sc, final DownloadConfig dc,
-      final GetArchive.AllowedFormats af,
-      @AnonymousCowardName final String acn) {
+  GerritConfigProvider(Realm r,
+      @GerritServerConfig Config gsc,
+      AuthConfig ac,
+      GitWebConfig gwc,
+      SshInfo si,
+      ServletContext sc,
+      GetArchive.AllowedFormats af,
+      @AnonymousCowardName String acn) {
     realm = r;
     cfg = gsc;
     authConfig = ac;
-    downloadConfig = dc;
     archiveFormats = af;
     gitWebConfig = gwc;
     sshInfo = si;
-    wildProject = wp;
-    contactStore = cs;
     servletContext = sc;
     anonymousCowardName = acn;
   }
 
-  @Inject(optional = true)
-  void setEmailSender(final EmailSender d) {
-    emailSender = d;
-  }
-
   private GerritConfig create() throws MalformedURLException {
     final GerritConfig config = new GerritConfig();
     switch (authConfig.getAuthType()) {
@@ -118,15 +102,9 @@
         break;
     }
     config.setSwitchAccountUrl(cfg.getString("auth", null, "switchAccountUrl"));
-    config.setUseContributorAgreements(cfg.getBoolean("auth",
-        "contributoragreements", false));
     config.setGitDaemonUrl(cfg.getString("gerrit", null, "canonicalgiturl"));
     config.setGitHttpUrl(cfg.getString("gerrit", null, "gitHttpUrl"));
-    config.setUseContactInfo(contactStore != null && contactStore.isEnabled());
-    config.setDownloadSchemes(downloadConfig.getDownloadSchemes());
-    config.setDownloadCommands(downloadConfig.getDownloadCommands());
     config.setAuthType(authConfig.getAuthType());
-    config.setWildProject(wildProject);
     config.setDocumentationAvailable(servletContext
         .getResource("/Documentation/index.html") != null);
     config.setAnonymousCowardName(anonymousCowardName);
@@ -134,8 +112,18 @@
     config.setChangeUpdateDelay((int) ConfigUtil.getTimeUnit(
         cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS));
     config.setLargeChangeSize(cfg.getInt("change", "largeChange", 500));
+
+    // Zip is not supported because it may be interpreted by a Java plugin as a
+    // valid JAR file, whose code would have access to cookies on the domain.
     config.setArchiveFormats(Lists.newArrayList(Iterables.transform(
-        archiveFormats.getAllowed(),
+        Iterables.filter(
+            archiveFormats.getAllowed(),
+            new Predicate<ArchiveFormat>() {
+              @Override
+              public boolean apply(ArchiveFormat format) {
+                return (format != ArchiveFormat.ZIP);
+              }
+            }),
         new Function<ArchiveFormat, String>() {
           @Override
           public String apply(ArchiveFormat in) {
@@ -146,17 +134,7 @@
     config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
     config.setReportBugText(cfg.getString("gerrit", null, "reportBugText"));
 
-    final Set<Account.FieldName> fields = new HashSet<>();
-    for (final Account.FieldName n : Account.FieldName.values()) {
-      if (realm.allowsEdit(n)) {
-        fields.add(n);
-      }
-    }
-    if (emailSender != null && emailSender.isEnabled()
-        && realm.allowsEdit(Account.FieldName.REGISTER_NEW_EMAIL)) {
-      fields.add(Account.FieldName.REGISTER_NEW_EMAIL);
-    }
-    config.setEditableAccountFields(fields);
+    config.setEditableAccountFields(realm.getEditableFields());
 
     if (gitWebConfig.getUrl() != null) {
       config.setGitwebLink(new GitwebConfig(gitWebConfig.getUrl(), gitWebConfig
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index ed84caf..c1c3b2b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -54,8 +54,6 @@
 @SuppressWarnings("serial")
 @Singleton
 class BecomeAnyAccountLoginServlet extends HttpServlet {
-  private static final boolean IS_DEV = Boolean.getBoolean("Gerrit.GwtDevMode");
-
   private final SchemaFactory<ReviewDb> schema;
   private final DynamicItem<WebSession> webSession;
   private final AccountManager accountManager;
@@ -120,14 +118,6 @@
       final StringBuilder rdr = new StringBuilder();
       rdr.append(req.getContextPath());
       rdr.append("/");
-      if (IS_DEV && req.getParameter("gwt.codesvr") != null) {
-        if (rdr.indexOf("?") < 0) {
-          rdr.append("?");
-        } else {
-          rdr.append("&");
-        }
-        rdr.append("gwt.codesvr=").append(req.getParameter("gwt.codesvr"));
-      }
 
       if (res.isNew()) {
         rdr.append('#' + PageLinks.REGISTER);
@@ -155,12 +145,6 @@
     if (doc == null) {
       throw new FileNotFoundException("No " + pageName + " in webapp");
     }
-    if (!IS_DEV) {
-      final Element devmode = HtmlDomUtil.find(doc, "gwtdevmode");
-      if (devmode != null) {
-        devmode.getParentNode().removeChild(devmode);
-      }
-    }
 
     Element userlistElement = HtmlDomUtil.find(doc, "userlist");
     ReviewDb db = schema.open();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
index 15150cf..cf43041 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
@@ -165,6 +165,8 @@
     myconfFile.setWritable(true, true /* owner only */);
     myconfFile.setReadable(true, true /* owner only */);
 
+    myconfFile.deleteOnExit();
+
     _env.set("GIT_DIR", ".");
     _env.set("GITWEB_CONFIG", myconf.toAbsolutePath().toString());
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index f3895b8..e0d4b51 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -72,7 +72,6 @@
 public class HostPageServlet extends HttpServlet {
   private static final Logger log =
       LoggerFactory.getLogger(HostPageServlet.class);
-  private static final boolean IS_DEV = Boolean.getBoolean("Gerrit.GwtDevMode");
   private static final String HPD_ID = "gerrit_hostpagedata";
   private static final int DEFAULT_JS_LOAD_TIMEOUT = 5000;
 
@@ -129,32 +128,25 @@
     }
 
     String src = "gerrit_ui/gerrit_ui.nocache.js";
-    if (!IS_DEV) {
-      Element devmode = HtmlDomUtil.find(template, "gwtdevmode");
-      if (devmode != null) {
-        devmode.getParentNode().removeChild(devmode);
-      }
-
-      InputStream in = servletContext.getResourceAsStream("/" + src);
-      if (in != null) {
-        Hasher md = Hashing.md5().newHasher();
+    InputStream in = servletContext.getResourceAsStream("/" + src);
+    if (in != null) {
+      Hasher md = Hashing.md5().newHasher();
+      try {
         try {
-          try {
-            final byte[] buf = new byte[1024];
-            int n;
-            while ((n = in.read(buf)) > 0) {
-              md.putBytes(buf, 0, n);
-            }
-          } finally {
-            in.close();
+          final byte[] buf = new byte[1024];
+          int n;
+          while ((n = in.read(buf)) > 0) {
+            md.putBytes(buf, 0, n);
           }
-        } catch (IOException e) {
-          throw new IOException("Failed reading " + src, e);
+        } finally {
+          in.close();
         }
-        src += "?content=" + md.hash().toString();
-      } else {
-        log.debug("No " + src + " in webapp root; keeping noncache.js URL");
+      } catch (IOException e) {
+        throw new IOException("Failed reading " + src, e);
       }
+      src += "?content=" + md.hash().toString();
+    } else {
+      log.debug("No " + src + " in webapp root; keeping noncache.js URL");
     }
 
     noCacheName = src;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index dca850ac..0045649 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -46,7 +46,7 @@
 import com.google.common.math.IntMath;
 import com.google.common.net.HttpHeaders;
 import com.google.gerrit.audit.AuditService;
-import com.google.gerrit.audit.HttpAuditEvent;
+import com.google.gerrit.audit.ExtendedHttpAuditEvent;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.registration.DynamicItem;
@@ -204,6 +204,8 @@
     Object result = null;
     Multimap<String, String> params = LinkedHashMultimap.create();
     Object inputRequestBody = null;
+    RestResource rsrc = TopLevelResource.INSTANCE;
+    ViewData viewData = null;
 
     try {
       checkUserSession(req);
@@ -213,8 +215,8 @@
       CapabilityUtils.checkRequiresCapability(globals.currentUser,
           null, rc.getClass());
 
-      RestResource rsrc = TopLevelResource.INSTANCE;
-      ViewData viewData = new ViewData(null, null);
+      viewData = new ViewData(null, null);
+
       if (path.isEmpty()) {
         if (isGetOrHead(req)) {
           viewData = new ViewData(null, rc.list());
@@ -386,10 +388,10 @@
       status = SC_INTERNAL_SERVER_ERROR;
       handleException(e, req, res);
     } finally {
-      globals.auditService.dispatch(new HttpAuditEvent(globals.webSession.get()
-          .getSessionId(), globals.currentUser.get(), req.getRequestURI(),
-          auditStartTs, params, req.getMethod(), inputRequestBody, status,
-          result));
+      globals.auditService.dispatch(new ExtendedHttpAuditEvent(globals.webSession.get()
+          .getSessionId(), globals.currentUser.get(), req,
+          auditStartTs, params, inputRequestBody, status,
+          result, rsrc, viewData == null ? null : viewData.view));
     }
   }
 
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
index c660311..23d5856 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
@@ -1,34 +1,6 @@
 <html>
   <head>
     <title>Gerrit Code Review</title>
-    <script id="gwtdevmode">
-      (function () {
-        var pn = 'gwt.codesvr';
-        var cn = 'gerrit_ui.' + pn;
-
-        var p_start = window.location.search.indexOf(pn + '=');
-        if (p_start != -1) {
-          p_start = p_start + pn.length + 1;
-          var p_end = window.location.search.indexOf(";", p_start);
-          if (p_end == -1) p_end = window.location.search.length;
-          var v = window.location.search.substring(p_start, p_end);
-
-          var e = new Date();
-          e.setDate(e.getDate() + 1);
-          document.cookie = cn + "=" + v + ';expires=' + e.toGMTString();
-
-        } else if (document.cookie.length != 0) {
-          var c_start = document.cookie.indexOf(cn + '=');
-          if (c_start != -1) {
-            c_start = c_start + cn.length + 1;
-            var c_end = document.cookie.indexOf(";", c_start);
-            if (c_end == -1) c_end = document.cookie.length;
-            var v = document.cookie.substring(c_start, c_end);
-            window.location.replace('?' + pn + '=' + v + document.location.hash);
-          }
-        }
-      })();
-    </script>
     <style id="gerrit_sitecss" type="text/css"></style>
   </head>
   <body>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
index 9f3fa1e..d2a333e 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
@@ -2,34 +2,6 @@
   <head>
     <title>Gerrit Code Review</title>
     <meta name="gwt:property" content="locale=en_US" />
-    <script id="gwtdevmode">
-      (function () {
-        var pn = 'gwt.codesvr';
-        var cn = 'gerrit_ui.' + pn;
-
-        var p_start = window.location.search.indexOf(pn + '=');
-        if (p_start != -1) {
-          p_start = p_start + pn.length + 1;
-          var p_end = window.location.search.indexOf(";", p_start);
-          if (p_end == -1) p_end = window.location.search.length;
-          var v = window.location.search.substring(p_start, p_end);
-
-          var e = new Date();
-          e.setDate(e.getDate() + 1);
-          document.cookie = cn + "=" + v + ';expires=' + e.toGMTString();
-
-        } else if (document.cookie.length != 0) {
-          var c_start = document.cookie.indexOf(cn + '=');
-          if (c_start != -1) {
-            c_start = c_start + cn.length + 1;
-            var c_end = document.cookie.indexOf(";", c_start);
-            if (c_end == -1) c_end = document.cookie.length;
-            var v = document.cookie.substring(c_start, c_end);
-            window.location.replace('?' + pn + '=' + v + document.location.hash);
-          }
-        }
-      })();
-    </script>
     <script id="gerrit_hostpagedata"></script>
     <style id="gerrit_sitecss" type="text/css"></style>
     <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
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 2b6c1281..64da702 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
@@ -31,7 +31,7 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.ChangeField.ChangeProtoField;
 import com.google.gerrit.server.index.ChangeField.PatchSetApprovalProtoField;
+import com.google.gerrit.server.index.ChangeField.PatchSetProtoField;
 import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.FieldDef.FillArgs;
@@ -51,6 +52,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
@@ -84,6 +86,7 @@
 import org.apache.lucene.search.Sort;
 import org.apache.lucene.search.SortField;
 import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.TopFieldDocs;
 import org.apache.lucene.store.RAMDirectory;
 import org.apache.lucene.uninverting.UninvertingReader;
 import org.apache.lucene.util.BytesRef;
@@ -97,6 +100,7 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -127,12 +131,13 @@
   private static final String ID_SORT_FIELD =
       sortFieldName(ChangeField.LEGACY_ID);
   private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
+  private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName();
   private static final String UPDATED_SORT_FIELD =
       sortFieldName(ChangeField.UPDATED);
 
   private static final ImmutableSet<String> FIELDS = ImmutableSet.of(
       ADDED_FIELD, APPROVAL_FIELD, CHANGE_FIELD, DELETED_FIELD, ID_FIELD,
-      MERGEABLE_FIELD);
+      MERGEABLE_FIELD, PATCH_SET_FIELD);
 
   private static final Map<String, String> CUSTOM_CHAR_MAPPING = ImmutableMap.of(
       "_", " ", ".", " ");
@@ -270,7 +275,7 @@
         ChangeField.UPDATED.getName(), UninvertingReader.Type.LONG);
     return new SearcherFactory() {
       @Override
-      public IndexSearcher newSearcher(IndexReader reader) {
+      public IndexSearcher newSearcher(IndexReader reader) throws IOException {
         checkState(reader instanceof DirectoryReader,
             "expected DirectoryReader, found %s", reader.getClass().getName());
         return new IndexSearcher(
@@ -411,7 +416,7 @@
       IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
       try {
         int realLimit = start + limit;
-        TopDocs[] hits = new TopDocs[indexes.size()];
+        TopFieldDocs[] hits = new TopFieldDocs[indexes.size()];
         for (int i = 0; i < indexes.size(); i++) {
           searchers[i] = indexes.get(i).acquire();
           hits[i] = searchers[i].search(query, realLimit, sort);
@@ -471,18 +476,19 @@
         cb.bytes, cb.offset, cb.length);
     ChangeData cd = changeDataFactory.create(db.get(), change);
 
-    // Approvals.
-    BytesRef[] approvalsBytes = doc.getBinaryValues(APPROVAL_FIELD);
-    if (approvalsBytes != null) {
-      List<PatchSetApproval> approvals =
-          Lists.newArrayListWithCapacity(approvalsBytes.length);
-      for (BytesRef ab : approvalsBytes) {
-        approvals.add(PatchSetApprovalProtoField.CODEC.decode(
-            ab.bytes, ab.offset, ab.length));
-      }
-      cd.setCurrentApprovals(approvals);
+    // Patch sets.
+    List<PatchSet> patchSets =
+        decodeProtos(doc, PATCH_SET_FIELD, PatchSetProtoField.CODEC);
+    if (!patchSets.isEmpty()) {
+      // Will be an empty list for schemas prior to when this field was stored;
+      // this cannot be valid since a change needs at least one patch set.
+      cd.setPatchSets(patchSets);
     }
 
+    // Approvals.
+    cd.setCurrentApprovals(
+        decodeProtos(doc, APPROVAL_FIELD, PatchSetApprovalProtoField.CODEC));
+
     // Changed lines.
     IndexableField added = doc.getField(ADDED_FIELD);
     IndexableField deleted = doc.getField(DELETED_FIELD);
@@ -503,6 +509,19 @@
     return cd;
   }
 
+  private static <T> List<T> decodeProtos(Document doc, String fieldName,
+      ProtobufCodec<T> codec) {
+    BytesRef[] bytesRefs = doc.getBinaryValues(fieldName);
+    if (bytesRefs.length == 0) {
+      return Collections.emptyList();
+    }
+    List<T> result = new ArrayList<>(bytesRefs.length);
+    for (BytesRef r : bytesRefs) {
+      result.add(codec.decode(r.bytes, r.offset, r.length));
+    }
+    return result;
+  }
+
   private Document toDocument(ChangeData cd) {
     Document result = new Document();
     for (Values<ChangeData> vs : schema.buildFields(cd, fillArgs)) {
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
index 4e4c774..36bca15 100644
--- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
@@ -50,7 +50,9 @@
   protected void doLogout(HttpServletRequest req, HttpServletResponse rsp)
       throws IOException {
     super.doLogout(req, rsp);
-    oauthSession.get().logout();
+    if (req.getSession(false) != null) {
+      oauthSession.get().logout();
+    }
   }
 }
 
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 739dffe..d24c8a0 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
@@ -26,11 +26,14 @@
 import com.google.gerrit.httpd.CanonicalWebUrl;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.servlet.SessionScoped;
 
 import org.apache.commons.codec.binary.Base64;
@@ -52,18 +55,22 @@
   private static final SecureRandom randomState = newRandomGenerator();
   private final String state;
   private final DynamicItem<WebSession> webSession;
+  private final Provider<IdentifiedUser> identifiedUser;
   private final AccountManager accountManager;
   private final CanonicalWebUrl urlProvider;
   private OAuthServiceProvider serviceProvider;
   private OAuthToken token;
   private OAuthUserInfo user;
   private String redirectToken;
+  private boolean linkMode;
 
   @Inject
   OAuthSession(DynamicItem<WebSession> webSession,
+      Provider<IdentifiedUser> identifiedUser,
       AccountManager accountManager,
       CanonicalWebUrl urlProvider) {
     this.state = generateRandomState();
+    this.identifiedUser = identifiedUser;
     this.webSession = webSession;
     this.accountManager = accountManager;
     this.urlProvider = urlProvider;
@@ -79,10 +86,6 @@
 
   boolean login(HttpServletRequest request, HttpServletResponse response,
       OAuthServiceProvider oauth) throws IOException {
-    if (isLoggedIn()) {
-      return true;
-    }
-
     log.debug("Login " + this);
 
     if (isOAuthFinal(request)) {
@@ -122,46 +125,19 @@
 
   private void authenticateAndRedirect(HttpServletRequest req,
       HttpServletResponse rsp) throws IOException {
-    com.google.gerrit.server.account.AuthRequest areq =
-        new com.google.gerrit.server.account.AuthRequest(user.getExternalId());
+    AuthRequest areq = new AuthRequest(user.getExternalId());
     AuthResult arsp;
     try {
       String claimedIdentifier = user.getClaimedIdentity();
-      Account.Id actualId = accountManager.lookup(user.getExternalId());
       if (!Strings.isNullOrEmpty(claimedIdentifier)) {
-        Account.Id claimedId = accountManager.lookup(claimedIdentifier);
-        if (claimedId != null && actualId != null) {
-          if (claimedId.equals(actualId)) {
-            // Both link to the same account, that's what we expected.
-            log.debug("OAuth2: claimed identity equals current id");
-          } else {
-            // This is (for now) a fatal error. There are two records
-            // for what might be the same user.
-            //
-            log.error("OAuth accounts disagree over user identity:\n"
-                + "  Claimed ID: " + claimedId + " is " + claimedIdentifier
-                + "\n" + "  Delgate ID: " + actualId + " is "
-                + user.getExternalId());
-            rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
-            return;
-          }
-        } else if (claimedId != null && actualId == null) {
-          // Claimed account already exists: link to it.
-          //
-          log.info("OAuth2: linking claimed identity to {}",
-              claimedId.toString());
-          try {
-            accountManager.link(claimedId, areq);
-          } catch (OrmException e) {
-            log.error("Cannot link: " +  user.getExternalId()
-                + " to user identity:\n"
-                + "  Claimed ID: " + claimedId + " is " + claimedIdentifier);
-            rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
-            return;
-          }
+        if (!authenticateWithIdentityClaimedDuringHandshake(areq, rsp,
+            claimedIdentifier)) {
+          return;
         }
-      } else {
-        log.debug("OAuth2: claimed identity is empty");
+      } else if (linkMode) {
+        if (!authenticateWithLinkedIdentity(areq, rsp)) {
+          return;
+        }
       }
       areq.setUserName(user.getUserName());
       areq.setEmailAddress(user.getEmailAddress());
@@ -181,6 +157,59 @@
     rsp.sendRedirect(rdr.toString());
   }
 
+  private boolean authenticateWithIdentityClaimedDuringHandshake(
+      AuthRequest req, HttpServletResponse rsp, String claimedIdentifier)
+      throws AccountException, IOException {
+    Account.Id claimedId = accountManager.lookup(claimedIdentifier);
+    Account.Id actualId = accountManager.lookup(user.getExternalId());
+    if (claimedId != null && actualId != null) {
+      if (claimedId.equals(actualId)) {
+        // Both link to the same account, that's what we expected.
+        log.debug("OAuth2: claimed identity equals current id");
+      } else {
+        // This is (for now) a fatal error. There are two records
+        // for what might be the same user.
+        //
+        log.error("OAuth accounts disagree over user identity:\n"
+            + "  Claimed ID: " + claimedId + " is " + claimedIdentifier
+            + "\n" + "  Delgate ID: " + actualId + " is "
+            + user.getExternalId());
+        rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+        return false;
+      }
+    } else if (claimedId != null && actualId == null) {
+      // Claimed account already exists: link to it.
+      //
+      log.info("OAuth2: linking claimed identity to {}",
+          claimedId.toString());
+      try {
+        accountManager.link(claimedId, req);
+      } catch (OrmException e) {
+        log.error("Cannot link: " +  user.getExternalId()
+            + " to user identity:\n"
+            + "  Claimed ID: " + claimedId + " is " + claimedIdentifier);
+        rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean authenticateWithLinkedIdentity(AuthRequest areq,
+      HttpServletResponse rsp) throws AccountException, IOException {
+    try {
+      accountManager.link(identifiedUser.get().getAccountId(), areq);
+    } catch (OrmException e) {
+      log.error("Cannot link: " + user.getExternalId()
+          + " to user identity: " + identifiedUser.get().getAccountId());
+      rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+      return false;
+    } finally {
+      linkMode = false;
+    }
+    return true;
+  }
+
   void logout() {
     token = null;
     user = null;
@@ -224,4 +253,12 @@
   public OAuthServiceProvider getServiceProvider() {
     return serviceProvider;
   }
+
+  public void setLinkMode(boolean linkMode) {
+    this.linkMode = linkMode;
+  }
+
+  public boolean isLinkMode() {
+    return linkMode;
+  }
 }
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
index 4021c57..2d73634 100644
--- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthWebFilter.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.template.SiteHeaderFooter;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -48,7 +47,6 @@
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
 
 @Singleton
 /* OAuth web filter uses active OAuth session to perform OAuth requests */
@@ -56,7 +54,6 @@
   static final String GERRIT_LOGIN = "/login";
 
   private final Provider<String> urlProvider;
-  private final Provider<CurrentUser> currentUserProvider;
   private final Provider<OAuthSession> oauthSessionProvider;
   private final DynamicMap<OAuthServiceProvider> oauthServiceProviders;
   private final SiteHeaderFooter header;
@@ -64,12 +61,10 @@
 
   @Inject
   OAuthWebFilter(@CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      Provider<CurrentUser> currentUserProvider,
       DynamicMap<OAuthServiceProvider> oauthServiceProviders,
       Provider<OAuthSession> oauthSessionProvider,
       SiteHeaderFooter header) {
     this.urlProvider = urlProvider;
-    this.currentUserProvider = currentUserProvider;
     this.oauthServiceProviders = oauthServiceProviders;
     this.oauthSessionProvider = oauthSessionProvider;
     this.header = header;
@@ -88,30 +83,20 @@
   public void doFilter(ServletRequest request, ServletResponse response,
       FilterChain chain) throws IOException, ServletException {
     HttpServletRequest httpRequest = (HttpServletRequest) request;
-    HttpSession httpSession = ((HttpServletRequest) request).getSession(false);
-    OAuthSession oauthSession = oauthSessionProvider.get();
-    if (currentUserProvider.get().isIdentifiedUser()) {
-      if (httpSession != null) {
-        httpSession.invalidate();
-      }
-      chain.doFilter(request, response);
-      return;
-    } else {
-      if (oauthSession.isLoggedIn()) {
-        oauthSession.logout();
-      }
-    }
-
     HttpServletResponse httpResponse = (HttpServletResponse) response;
 
+    OAuthSession oauthSession = oauthSessionProvider.get();
+    if (request.getParameter("link") != null) {
+      oauthSession.setLinkMode(true);
+      oauthSession.setServiceProvider(null);
+    }
+
     String provider = httpRequest.getParameter("provider");
     OAuthServiceProvider service = ssoProvider == null
         ? oauthSession.getServiceProvider()
         : ssoProvider;
 
-    if ((isGerritLogin(httpRequest)
-        || oauthSession.isOAuthFinal(httpRequest))
-        && !oauthSession.isLoggedIn()) {
+    if (isGerritLogin(httpRequest) || oauthSession.isOAuthFinal(httpRequest)) {
       if (service == null && Strings.isNullOrEmpty(provider)) {
         selectProvider(httpRequest, httpResponse, null);
         return;
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
index bef165b..b8080c9 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -175,9 +175,9 @@
         oauthSession.logout();
       }
       if ((isGerritLogin(req)
-          || oauthSession.isOAuthFinal(req))
-          && !oauthSession.isLoggedIn()) {
+          || oauthSession.isOAuthFinal(req))) {
         oauthSession.setServiceProvider(oauthProvider);
+        oauthSession.setLinkMode(link);
         oauthSession.login(req, res, oauthProvider);
       }
     }
@@ -304,7 +304,7 @@
           oauthServiceProviders.byPlugin(pluginName);
         for (Map.Entry<String, Provider<OAuthServiceProvider>> e
             : m.entrySet()) {
-          addProvider(providers, pluginName, e.getKey(),
+          addProvider(providers, link, pluginName, e.getKey(),
               e.getValue().get().getName());
         }
     }
@@ -327,13 +327,18 @@
     }
   }
 
-  private static void addProvider(Element form, String pluginName,
-      String id, String serviceName) {
+  private static void addProvider(Element form, boolean link,
+      String pluginName, String id, String serviceName) {
     Element div = form.getOwnerDocument().createElement("div");
     div.setAttribute("id", id);
     Element hyperlink = form.getOwnerDocument().createElement("a");
-    hyperlink.setAttribute("href", String.format("?id=%s_%s",
+    StringBuilder u = new StringBuilder(String.format("?id=%s_%s",
         pluginName, id));
+    if (link) {
+      u.append("&link");
+    }
+    hyperlink.setAttribute("href", u.toString());
+
     hyperlink.setTextContent(serviceName +
         " (" + pluginName + " plugin)");
     div.appendChild(hyperlink);
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
index 9dbff03..02f428e 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
@@ -50,6 +50,8 @@
   protected void doLogout(HttpServletRequest req, HttpServletResponse rsp)
       throws IOException {
     super.doLogout(req, rsp);
-    oauthSession.get().logout();
+    if (req.getSession(false) != null) {
+      oauthSession.get().logout();
+    }
   }
 }
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index a02f52d..6d129bf 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -27,11 +27,13 @@
 import com.google.gerrit.httpd.LoginUrlToken;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.servlet.SessionScoped;
 
 import org.apache.commons.codec.binary.Base64;
@@ -55,19 +57,23 @@
   private static final SecureRandom randomState = newRandomGenerator();
   private final String state;
   private final DynamicItem<WebSession> webSession;
+  private final Provider<IdentifiedUser> identifiedUser;
   private final AccountManager accountManager;
   private final CanonicalWebUrl urlProvider;
   private OAuthServiceProvider serviceProvider;
   private OAuthToken token;
   private OAuthUserInfo user;
   private String redirectToken;
+  private boolean linkMode;
 
   @Inject
   OAuthSessionOverOpenID(DynamicItem<WebSession> webSession,
+      Provider<IdentifiedUser> identifiedUser,
       AccountManager accountManager,
       CanonicalWebUrl urlProvider) {
     this.state = generateRandomState();
     this.webSession = webSession;
+    this.identifiedUser = identifiedUser;
     this.accountManager = accountManager;
     this.urlProvider = urlProvider;
   }
@@ -82,10 +88,6 @@
 
   boolean login(HttpServletRequest request, HttpServletResponse response,
       OAuthServiceProvider oauth) throws IOException {
-    if (isLoggedIn()) {
-      return true;
-    }
-
     log.debug("Login " + this);
 
     if (isOAuthFinal(request)) {
@@ -96,7 +98,6 @@
 
       log.debug("Login-Retrieve-User " + this);
       token = oauth.getAccessToken(new OAuthVerifier(request.getParameter("code")));
-
       user = oauth.getUserInfo(token);
 
       if (isLoggedIn()) {
@@ -124,6 +125,7 @@
     try {
       String claimedIdentifier = user.getClaimedIdentity();
       Account.Id actualId = accountManager.lookup(user.getExternalId());
+      // Use case 1: claimed identity was provided during handshake phase
       if (!Strings.isNullOrEmpty(claimedIdentifier)) {
         Account.Id claimedId = accountManager.lookup(claimedIdentifier);
         if (claimedId != null && actualId != null) {
@@ -153,6 +155,18 @@
             return;
           }
         }
+      } else if (linkMode) {
+        // Use case 2: link mode activated from the UI
+        try {
+          accountManager.link(identifiedUser.get().getAccountId(), areq);
+        } catch (OrmException e) {
+          log.error("Cannot link: " + user.getExternalId()
+              + " to user identity: " + identifiedUser.get().getAccountId());
+          rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
+          return;
+        } finally {
+          linkMode = false;
+        }
       }
       areq.setUserName(user.getUserName());
       areq.setEmailAddress(user.getEmailAddress());
@@ -213,4 +227,12 @@
   public OAuthServiceProvider getServiceProvider() {
     return serviceProvider;
   }
+
+  public void setLinkMode(boolean linkMode) {
+    this.linkMode = linkMode;
+  }
+
+  public boolean isLinkMode() {
+    return linkMode;
+  }
 }
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
index 7766e69..c17079d 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthWebFilterOverOpenID.java
@@ -17,7 +17,6 @@
 import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider;
 import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.server.CurrentUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -34,7 +33,6 @@
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
 
 
 /** OAuth web filter uses active OAuth session to perform OAuth requests */
@@ -42,16 +40,13 @@
 class OAuthWebFilterOverOpenID implements Filter {
   static final String GERRIT_LOGIN = "/login";
 
-  private final Provider<CurrentUser> currentUserProvider;
   private final Provider<OAuthSessionOverOpenID> oauthSessionProvider;
   private final DynamicMap<OAuthServiceProvider> oauthServiceProviders;
   private OAuthServiceProvider ssoProvider;
 
   @Inject
-  OAuthWebFilterOverOpenID(Provider<CurrentUser> currentUserProvider,
-      DynamicMap<OAuthServiceProvider> oauthServiceProviders,
+  OAuthWebFilterOverOpenID(DynamicMap<OAuthServiceProvider> oauthServiceProviders,
       Provider<OAuthSessionOverOpenID> oauthSessionProvider) {
-    this.currentUserProvider = currentUserProvider;
     this.oauthServiceProviders = oauthServiceProviders;
     this.oauthSessionProvider = oauthSessionProvider;
   }
@@ -69,15 +64,6 @@
   public void doFilter(ServletRequest request, ServletResponse response,
       FilterChain chain) throws IOException, ServletException {
     HttpServletRequest httpRequest = (HttpServletRequest) request;
-    HttpSession httpSession = ((HttpServletRequest) request).getSession(false);
-    if (currentUserProvider.get().isIdentifiedUser()) {
-      if (httpSession != null) {
-        httpSession.invalidate();
-      }
-      chain.doFilter(request, response);
-      return;
-    }
-
     HttpServletResponse httpResponse = (HttpServletResponse) response;
 
     OAuthSessionOverOpenID oauthSession = oauthSessionProvider.get();
@@ -85,9 +71,7 @@
         ? oauthSession.getServiceProvider()
         : ssoProvider;
 
-    if ((isGerritLogin(httpRequest)
-        || oauthSession.isOAuthFinal(httpRequest))
-        && !oauthSession.isLoggedIn()) {
+    if (isGerritLogin(httpRequest) || oauthSession.isOAuthFinal(httpRequest)) {
         if (service == null) {
           throw new IllegalStateException("service is unknown");
         }
diff --git a/gerrit-pgm/BUCK b/gerrit-pgm/BUCK
index 8c1ffd3..10d6dda 100644
--- a/gerrit-pgm/BUCK
+++ b/gerrit-pgm/BUCK
@@ -18,7 +18,8 @@
     '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
     '//lib/log:api',
-    '//lib/log:log4j',
+    '//lib/log:jsonevent-layout',
+    '//lib/log:log4j'
 ]
 
 java_library(
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 00a1ade..1afa243 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -54,6 +54,7 @@
 import com.google.gerrit.server.config.RestCacheAdminModule;
 import com.google.gerrit.server.contact.ContactStoreModule;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
+import com.google.gerrit.server.git.ChangeCacheImplModule;
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
@@ -196,10 +197,6 @@
       throw die("No services enabled, nothing to do");
     }
 
-    if (!consoleLog) {
-      manager.add(ErrorLogFile.start(getSitePath()));
-    }
-
     try {
       start();
       RuntimeShutdown.add(new Runnable() {
@@ -272,7 +269,7 @@
   }
 
   @VisibleForTesting
-  public void start() {
+  public void start() throws IOException {
     if (dbInjector == null) {
       dbInjector = createDbInjector(MULTI_USER);
     }
@@ -282,6 +279,11 @@
       .setDbCfgInjector(dbInjector, cfgInjector);
     manager.add(dbInjector, cfgInjector, sysInjector);
 
+    if (!consoleLog) {
+      manager.add(ErrorLogFile.start(getSitePath(),
+          cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class))));
+    }
+
     sshd &= !sshdOff();
     if (sshd) {
       initSshd();
@@ -324,6 +326,7 @@
     modules.add(new DiffExecutorModule());
     modules.add(new MimeUtil2Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+    modules.add(new ChangeCacheImplModule(slave));
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultCacheFactory.Module());
     if (emailModule != null) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
index 547c1a9..ff157ce 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
@@ -191,9 +191,9 @@
           + " is not found or not executable");
       }
     } catch (InvocationTargetException e) {
-      log.error("Exception occured while loading file " + p + " : ", e);
+      log.error("Exception occurred while loading file " + p + " : ", e);
     } catch (SecurityException e) {
-      log.error("SecurityException occured while loading file " + p + " : ", e);
+      log.error("SecurityException occurred while loading file " + p + " : ", e);
     }
   }
 
@@ -204,7 +204,7 @@
         new Object[] { in, p }
       );
     } catch (InvocationTargetException e) {
-      log.error("Exception occured while loading " + p + " : ", e);
+      log.error("Exception occurred while loading " + p + " : ", e);
     }
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index c1f0090..e0cb4c4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.SchemaFactory;
@@ -30,6 +31,11 @@
 
 import org.apache.commons.validator.routines.EmailValidator;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Collections;
 
 public class InitAdminUser implements InitStep {
@@ -70,8 +76,9 @@
           Account.Id id = new Account.Id(db.nextAccountId());
           String username = ui.readString("admin", "username");
           String name = ui.readString("Administrator", "name");
-          String email = readEmail();
           String httpPassword = ui.readString("secret", "HTTP password");
+          AccountSshKey sshKey = readSshKey(id);
+          String email = readEmail(sshKey);
 
           AccountExternalId extUser =
               new AccountExternalId(id, new AccountExternalId.Key(
@@ -98,6 +105,10 @@
               new AccountGroupMember(new AccountGroupMember.Key(id,
                   new AccountGroup.Id(1)));
           db.accountGroupMembers().insert(Collections.singleton(m));
+
+          if (sshKey != null) {
+            db.accountSshKeys().insert(Collections.singleton(sshKey));
+          }
         }
       }
     } finally {
@@ -105,12 +116,47 @@
     }
   }
 
-  private String readEmail() {
-    String email = ui.readString("admin@example.com", "email");
+  private String readEmail(AccountSshKey sshKey) {
+    String defaultEmail = "admin@example.com";
+    if (sshKey != null && sshKey.getComment() != null) {
+     String c = sshKey.getComment().trim();
+     if (EmailValidator.getInstance().isValid(c)) {
+       defaultEmail = c;
+     }
+    }
+    return readEmail(defaultEmail);
+  }
+
+  private String readEmail(String defaultEmail) {
+    String email = ui.readString(defaultEmail, "email");
     if (email != null && !EmailValidator.getInstance().isValid(email)) {
       ui.message("error: invalid email address\n");
-      return readEmail();
+      return readEmail(defaultEmail);
     }
     return email;
   }
+
+  private AccountSshKey readSshKey(Account.Id id) throws IOException {
+    String defaultPublicSshKeyFile = "";
+    Path defaultPublicSshKeyPath =
+        Paths.get(System.getProperty("user.home"), ".ssh", "id_rsa.pub");
+    if (Files.exists(defaultPublicSshKeyPath)) {
+      defaultPublicSshKeyFile = defaultPublicSshKeyPath.toString();
+    }
+    String publicSshKeyFile =
+        ui.readString(defaultPublicSshKeyFile, "public SSH key file");
+    return !Strings.isNullOrEmpty(publicSshKeyFile)
+        ? createSshKey(id, publicSshKeyFile) : null;
+  }
+
+  private AccountSshKey createSshKey(Account.Id id, String keyFile)
+      throws IOException {
+    Path p = Paths.get(keyFile);
+    if (!Files.exists(p)) {
+      throw new IOException(String.format(
+          "Cannot add public SSH key: %s is not a file", keyFile));
+    }
+    String content = new String(Files.readAllBytes(p), StandardCharsets.UTF_8);
+    return new AccountSshKey(new AccountSshKey.Id(id, 0), content);
+  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
index b8a618b..ee0a0e6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -19,17 +19,21 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.util.SystemLog;
 
+import net.logstash.log4j.JSONEventLayoutV1;
+
 import org.apache.log4j.ConsoleAppender;
 import org.apache.log4j.Level;
 import org.apache.log4j.LogManager;
 import org.apache.log4j.Logger;
 import org.apache.log4j.PatternLayout;
+import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
 import java.nio.file.Path;
 
 public class ErrorLogFile {
   static final String LOG_NAME = "error_log";
+  static final String JSON_SUFFIX = ".json";
 
   public static void errorOnlyConsole() {
     LogManager.resetConfiguration();
@@ -47,12 +51,12 @@
     root.addAppender(dst);
   }
 
-  public static LifecycleListener start(final Path sitePath)
+  public static LifecycleListener start(final Path sitePath, final Config config)
       throws IOException {
     Path logdir = FileUtil.mkdirsOrDie(new SitePaths(sitePath).logs_dir,
         "Cannot create log directory");
     if (SystemLog.shouldConfigure()) {
-      initLogSystem(logdir);
+      initLogSystem(logdir, config);
     }
 
     return new LifecycleListener() {
@@ -67,10 +71,21 @@
     };
   }
 
-  private static void initLogSystem(Path logdir) {
+  private static void initLogSystem(Path logdir, Config config) {
     final Logger root = LogManager.getRootLogger();
     root.removeAllAppenders();
-    root.addAppender(SystemLog.createAppender(logdir, LOG_NAME,
-        new PatternLayout("[%d] %-5p %c %x: %m%n")));
+
+    boolean json = config.getBoolean("log", "jsonLogging", false);
+    boolean text = config.getBoolean("log", "textLogging", true) || !json;
+
+    if (text) {
+      root.addAppender(SystemLog.createAppender(logdir, LOG_NAME,
+          new PatternLayout("[%d] %-5p %c %x: %m%n")));
+    }
+
+    if (json) {
+      root.addAppender(SystemLog.createAppender(logdir, LOG_NAME + JSON_SUFFIX,
+          new JSONEventLayoutV1()));
+    }
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
index a3ad822..3f04306 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
@@ -19,9 +19,6 @@
 
 /** Line of development within a {@link Project}. */
 public final class Branch {
-  public static final String R_HEADS = "refs/heads/";
-  public static final String R_REFS = "refs/";
-
   /** Branch name key */
   public static class NameKey extends StringKey<Project.NameKey> {
     private static final long serialVersionUID = 1L;
@@ -57,15 +54,7 @@
     }
 
     public String getShortName() {
-      final String n = get();
-
-      // Git style branches will tend to start with "refs/heads/".
-      //
-      if (n.startsWith(R_HEADS)) {
-        return n.substring(R_HEADS.length());
-      }
-
-      return n;
+      return RefNames.shortName(get());
     }
   }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
index 1719a7e..e6015e6 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -56,6 +56,12 @@
     return (ref.startsWith(REFS) ? "" : REFS_HEADS) + ref;
   }
 
+  public static final String shortName(String ref) {
+    return ref.startsWith(REFS_HEADS)
+        ? ref.substring(REFS_HEADS.length())
+        : ref;
+  }
+
   public static String refsUsers(Account.Id accountId) {
     StringBuilder r = new StringBuilder();
     r.append(REFS_USER);
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
index 1a64ee7..a73fe5d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
@@ -64,4 +64,9 @@
   public boolean equals(Object o) {
     return (o instanceof RevId) && id.equals(((RevId) o).id);
   }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "{" + id + "}";
+  }
 }
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
index 33da24a..4bf1d81 100644
--- a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
+++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/PatchSetTest.java
@@ -14,10 +14,7 @@
 
 package com.google.gerrit.reviewdb.client;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
 
 import org.junit.Test;
 
@@ -63,13 +60,13 @@
   }
 
   private static void assertRef(int changeId, int psId, String refName) {
-    assertTrue(PatchSet.isRef(refName));
-    assertEquals(new PatchSet.Id(new Change.Id(changeId), psId),
-        PatchSet.Id.fromRef(refName));
+    assertThat(PatchSet.isRef(refName)).isTrue();
+    assertThat(PatchSet.Id.fromRef(refName))
+        .isEqualTo(new PatchSet.Id(new Change.Id(changeId), psId));
   }
 
   private static void assertNotRef(String refName) {
-    assertFalse(PatchSet.isRef(refName));
-    assertNull(PatchSet.Id.fromRef(refName));
+    assertThat(PatchSet.isRef(refName)).isFalse();
+    assertThat(PatchSet.Id.fromRef(refName)).isNull();
   }
 }
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index ec91d49..5cda4dd 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -48,6 +48,7 @@
     '//lib/antlr:java_runtime',
     '//lib/auto:auto-value',
     '//lib/commons:codec',
+    '//lib/commons:compress',
     '//lib/commons:dbcp',
     '//lib/commons:lang',
     '//lib/commons:net',
@@ -59,6 +60,7 @@
     '//lib/jgit:jgit-archive',
     '//lib/joda:joda-time',
     '//lib/log:api',
+    '//lib/log:jsonevent-layout',
     '//lib/log:log4j',
     '//lib/lucene:analyzers-common',
     '//lib/lucene:core',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java
new file mode 100644
index 0000000..78fe38c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/ExtendedHttpAuditEvent.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.audit;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Extended audit event. Adds request, resource and view data to HttpAuditEvent.
+ */
+public class ExtendedHttpAuditEvent extends HttpAuditEvent {
+  public final HttpServletRequest httpRequest;
+  public final RestResource resource;
+  public final RestView<? extends RestResource> view;
+
+  /**
+   * Creates a new audit event with results
+   *
+   * @param sessionId session id the event belongs to
+   * @param who principal that has generated the event
+   * @param httpRequest the HttpServletRequest
+   * @param when time-stamp of when the event started
+   * @param params parameters of the event
+   * @param result result of the event
+   * @param resource REST resource data
+   * @param view view rendering object
+   */
+  public ExtendedHttpAuditEvent(String sessionId, CurrentUser who,
+      HttpServletRequest httpRequest, long when, Multimap<String, ?> params,
+      Object input, int status, Object result, RestResource resource,
+      RestView<RestResource> view) {
+    super(sessionId, who, httpRequest.getRequestURI(), when, params, httpRequest.getMethod(),
+        input, status, result);
+    this.httpRequest = Preconditions.checkNotNull(httpRequest);
+    this.resource = resource;
+    this.view = view;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 169e03b..c9359b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -50,7 +50,6 @@
 public final class StoredValues {
   public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
   public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class);
-  public static final StoredValue<PatchSet> PATCH_SET = create(PatchSet.class);
 
   // Note: no guarantees are made about the user passed in the ChangeControl; do
   // not depend on this directly. Either use .forUser(otherUser) to get a
@@ -68,11 +67,20 @@
     }
   }
 
+  public static PatchSet getPatchSet(Prolog engine) throws SystemException {
+    ChangeData cd = CHANGE_DATA.get(engine);
+    try {
+      return cd.currentPatchSet();
+    } catch (OrmException e) {
+      throw new SystemException(e.getMessage());
+    }
+  }
+
   public static final StoredValue<PatchSetInfo> PATCH_SET_INFO = new StoredValue<PatchSetInfo>() {
     @Override
     public PatchSetInfo createValue(Prolog engine) {
       Change change = getChange(engine);
-      PatchSet ps = StoredValues.PATCH_SET.get(engine);
+      PatchSet ps = getPatchSet(engine);
       PrologEnvironment env = (PrologEnvironment) engine.control;
       PatchSetInfoFactory patchInfoFactory =
               env.getArgs().getPatchSetInfoFactory();
@@ -88,7 +96,7 @@
     @Override
     public PatchList createValue(Prolog engine) {
       PrologEnvironment env = (PrologEnvironment) engine.control;
-      PatchSet ps = StoredValues.PATCH_SET.get(engine);
+      PatchSet ps = getPatchSet(engine);
       PatchListCache plCache = env.getArgs().getPatchListCache();
       Change change = getChange(engine);
       Project.NameKey projectKey = change.getProject();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index bd175a9..c31a411 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -142,7 +142,7 @@
 
   private static TreeMap<Integer, PatchSet> getPatchSets(ChangeData cd)
       throws OrmException {
-    Collection<PatchSet> patchSets = cd.patches();
+    Collection<PatchSet> patchSets = cd.patchSets();
     TreeMap<Integer, PatchSet> result = Maps.newTreeMap();
     for (PatchSet ps : patchSets) {
       result.put(ps.getId().get(), ps);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index f10837f..305b66d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -17,9 +17,11 @@
 import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy.RECEIVE_COMMITS;
 import static com.google.gerrit.server.query.change.ChangeData.asChanges;
 
+import com.google.common.base.Function;
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.reviewdb.client.Change;
@@ -95,6 +97,17 @@
   private static final Logger log =
       LoggerFactory.getLogger(ChangeUtil.class);
 
+  public static final Function<PatchSet, Integer> TO_PS_ID =
+      new Function<PatchSet, Integer>() {
+        @Override
+        public Integer apply(PatchSet in) {
+          return in.getId().get();
+        }
+      };
+
+  public static final Ordering<PatchSet> PS_ID_ORDER = Ordering.natural()
+    .onResultOf(TO_PS_ID);
+
   /**
    * Generate a new unique identifier for change message entities.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
index f67fa75..88f034e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchLineCommentsUtil.java
@@ -14,11 +14,18 @@
 
 package com.google.gerrit.server;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
+
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
@@ -61,6 +68,47 @@
  */
 @Singleton
 public class PatchLineCommentsUtil {
+  public static Ordering<PatchLineComment> PLC_ORDER =
+      new Ordering<PatchLineComment>() {
+    @Override
+    public int compare(PatchLineComment c1, PatchLineComment c2) {
+      String filename1 = c1.getKey().getParentKey().get();
+      String filename2 = c2.getKey().getParentKey().get();
+      return ComparisonChain.start()
+          .compare(filename1, filename2)
+          .compare(getCommentPsId(c1).get(), getCommentPsId(c2).get())
+          .compare(c1.getSide(), c2.getSide())
+          .compare(c1.getLine(), c2.getLine())
+          .compare(c1.getWrittenOn(), c2.getWrittenOn())
+          .result();
+    }
+  };
+
+  public static final Ordering<CommentInfo> COMMENT_INFO_ORDER =
+      new Ordering<CommentInfo>() {
+        @Override
+        public int compare(CommentInfo a, CommentInfo b) {
+          return ComparisonChain.start()
+              .compare(a.path, b.path, NULLS_FIRST)
+              .compare(a.patchSet, b.patchSet, NULLS_FIRST)
+              .compare(side(a), side(b))
+              .compare(a.line, b.line, NULLS_FIRST)
+              .compare(a.id, b.id)
+              .result();
+        }
+
+        private int side(CommentInfo c) {
+          return firstNonNull(c.side, Side.REVISION).ordinal();
+        }
+      };
+
+  public static PatchSet.Id getCommentPsId(PatchLineComment plc) {
+    return plc.getKey().getParentKey().getParentKey();
+  }
+
+  private static final Ordering<Comparable<?>> NULLS_FIRST =
+      Ordering.natural().nullsFirst();
+
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
   private final DraftCommentNotes.Factory draftFactory;
@@ -105,8 +153,7 @@
 
     notes.load();
     List<PatchLineComment> comments = Lists.newArrayList();
-    comments.addAll(notes.getBaseComments().values());
-    comments.addAll(notes.getPatchSetComments().values());
+    comments.addAll(notes.getComments().values());
     return sort(comments);
   }
 
@@ -165,13 +212,7 @@
       return sort(
           db.patchComments().publishedByChangeFile(changeId, file).toList());
     }
-    notes.load();
-    List<PatchLineComment> comments = Lists.newArrayList();
-
-    addCommentsOnFile(comments, notes.getBaseComments().values(), file);
-    addCommentsOnFile(comments, notes.getPatchSetComments().values(),
-        file);
-    return sort(comments);
+    return commentsOnFile(notes.load().getComments().values(), file);
   }
 
   public List<PatchLineComment> publishedByPatchSet(ReviewDb db,
@@ -180,11 +221,7 @@
       return sort(
           db.patchComments().publishedByPatchSet(psId).toList());
     }
-    notes.load();
-    List<PatchLineComment> comments = new ArrayList<>();
-    comments.addAll(notes.getPatchSetComments().get(psId));
-    comments.addAll(notes.getBaseComments().get(psId));
-    return sort(comments);
+    return commentsOnPatchSet(notes.load().getComments().values(), psId);
   }
 
   public List<PatchLineComment> draftByPatchSetAuthor(ReviewDb db,
@@ -194,11 +231,8 @@
       return sort(
           db.patchComments().draftByPatchSetAuthor(psId, author).toList());
     }
-
-    List<PatchLineComment> comments = Lists.newArrayList();
-    comments.addAll(notes.getDraftBaseComments(author).row(psId).values());
-    comments.addAll(notes.getDraftPsComments(author).row(psId).values());
-    return sort(comments);
+    return commentsOnPatchSet(
+        notes.load().getDraftComments(author).values(), psId);
   }
 
   public List<PatchLineComment> draftByChangeFileAuthor(ReviewDb db,
@@ -210,23 +244,28 @@
             .draftByChangeFileAuthor(notes.getChangeId(), file, author)
             .toList());
     }
-    List<PatchLineComment> comments = Lists.newArrayList();
-    addCommentsOnFile(comments, notes.getDraftBaseComments(author).values(),
-        file);
-    addCommentsOnFile(comments, notes.getDraftPsComments(author).values(),
-        file);
-    return sort(comments);
+    return commentsOnFile(
+        notes.load().getDraftComments(author).values(), file);
   }
 
   public List<PatchLineComment> draftByChangeAuthor(ReviewDb db,
       ChangeNotes notes, Account.Id author)
       throws OrmException {
     if (!migration.readChanges()) {
-      return sort(db.patchComments().byChange(notes.getChangeId()).toList());
+      final Change.Id matchId = notes.getChangeId();
+      return FluentIterable
+          .from(db.patchComments().draftByAuthor(author))
+          .filter(new Predicate<PatchLineComment>() {
+            @Override
+            public boolean apply(PatchLineComment in) {
+              Change.Id changeId =
+                  in.getKey().getParentKey().getParentKey().getParentKey();
+              return changeId.equals(matchId);
+            }
+          }).toSortedList(PLC_ORDER);
     }
     List<PatchLineComment> comments = Lists.newArrayList();
-    comments.addAll(notes.getDraftBaseComments(author).values());
-    comments.addAll(notes.getDraftPsComments(author).values());
+    comments.addAll(notes.getDraftComments(author).values());
     return sort(comments);
   }
 
@@ -236,9 +275,8 @@
       return sort(db.patchComments().draftByAuthor(author).toList());
     }
 
-    Set<String> refNames =
-        getRefNamesAllUsers(RefNames.REFS_DRAFT_COMMENTS);
-
+    // TODO(dborowitz): Just scan author space.
+    Set<String> refNames = getRefNamesAllUsers(RefNames.REFS_DRAFT_COMMENTS);
     List<PatchLineComment> comments = Lists.newArrayList();
     for (String refName : refNames) {
       Account.Id id = Account.Id.fromRefPart(refName);
@@ -246,10 +284,8 @@
         continue;
       }
       Change.Id changeId = Change.Id.parse(refName);
-      DraftCommentNotes draftNotes =
-          draftFactory.create(changeId, author).load();
-      comments.addAll(draftNotes.getDraftBaseComments().values());
-      comments.addAll(draftNotes.getDraftPsComments().values());
+      comments.addAll(
+          draftFactory.create(changeId, author).load().getComments().values());
     }
     return sort(comments);
   }
@@ -286,33 +322,45 @@
     db.patchComments().delete(comments);
   }
 
-  private static Collection<PatchLineComment> addCommentsOnFile(
-      Collection<PatchLineComment> commentsOnFile,
+  private static List<PatchLineComment> commentsOnFile(
       Collection<PatchLineComment> allComments,
       String file) {
+    List<PatchLineComment> result = new ArrayList<>(allComments.size());
     for (PatchLineComment c : allComments) {
       String currentFilename = c.getKey().getParentKey().getFileName();
       if (currentFilename.equals(file)) {
-        commentsOnFile.add(c);
+        result.add(c);
       }
     }
-    return commentsOnFile;
+    return sort(result);
   }
 
-  public static void setCommentRevId(PatchLineComment c,
+  private static List<PatchLineComment> commentsOnPatchSet(
+      Collection<PatchLineComment> allComments,
+      PatchSet.Id psId) {
+    List<PatchLineComment> result = new ArrayList<>(allComments.size());
+    for (PatchLineComment c : allComments) {
+      if (getCommentPsId(c).equals(psId)) {
+        result.add(c);
+      }
+    }
+    return sort(result);
+  }
+
+  public static RevId setCommentRevId(PatchLineComment c,
       PatchListCache cache, Change change, PatchSet ps) throws OrmException {
-    if (c.getRevId() != null) {
-      return;
+    if (c.getRevId() == null) {
+      try {
+        // TODO(dborowitz): Bypass cache if side is REVISION.
+        PatchList patchList = cache.get(change, ps);
+        c.setRevId((c.getSide() == (short) 0)
+          ? new RevId(ObjectId.toString(patchList.getOldId()))
+          : new RevId(ObjectId.toString(patchList.getNewId())));
+      } catch (PatchListNotAvailableException e) {
+        throw new OrmException(e);
+      }
     }
-    PatchList patchList;
-    try {
-      patchList = cache.get(change, ps);
-    } catch (PatchListNotAvailableException e) {
-      throw new OrmException(e);
-    }
-    c.setRevId((c.getSide() == (short) 0)
-      ? new RevId(ObjectId.toString(patchList.getOldId()))
-      : new RevId(ObjectId.toString(patchList.getNewId())));
+    return c.getRevId();
   }
 
   private Set<String> getRefNamesAllUsers(String prefix) throws OrmException {
@@ -345,7 +393,7 @@
   }
 
   private static List<PatchLineComment> sort(List<PatchLineComment> comments) {
-    Collections.sort(comments, ChangeNotes.PatchLineCommentComparator);
+    Collections.sort(comments, PLC_ORDER);
     return comments;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
index fb28a55..465c9ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
@@ -263,7 +263,7 @@
     }
   }
 
-  public class PermissionInfo {
+  public static class PermissionInfo {
     public String label;
     public Boolean exclusive;
     public Map<String, PermissionRuleInfo> rules;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
index 7031672..34f83f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -16,15 +16,44 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.EmailSender;
+import com.google.inject.Inject;
 
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Objects;
 import java.util.Set;
 
 /** Basic implementation of {@link Realm}.  */
 public abstract class AbstractRealm implements Realm {
+  private EmailSender emailSender;
+
+  @Inject(optional = true)
+  void setEmailSender(EmailSender emailSender) {
+    this.emailSender = emailSender;
+  }
+
+  @Override
+  public Set<FieldName> getEditableFields() {
+    Set<Account.FieldName> fields = new  HashSet<>();
+    for (Account.FieldName n : Account.FieldName.values()) {
+      if (allowsEdit(n)) {
+        if (n == Account.FieldName.REGISTER_NEW_EMAIL) {
+          if (emailSender != null && emailSender.isEnabled()) {
+            fields.add(n);
+          }
+        } else {
+          fields.add(n);
+        }
+      }
+    }
+    return fields;
+  }
+
   @Override
   public boolean hasEmailAddress(IdentifiedUser user, String email) {
     for (AccountExternalId ext : user.state().getExternalIds()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
index 445ac6e..b4ca530 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountDirectory.java
@@ -39,7 +39,7 @@
     USERNAME,
 
     /** Numeric account ID, may be deprecated. */
-    ID;
+    ID
   }
 
   public abstract void fillAccountInfo(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index 8dd8de7..056fa85b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -24,6 +24,9 @@
   /** Can the end-user modify this field of their own account? */
   public boolean allowsEdit(Account.FieldName field);
 
+  /** Returns the account fields that the end-user can modify. */
+  public Set<Account.FieldName> getEditableFields();
+
   public AuthRequest authenticate(AuthRequest who) throws AccountException;
 
   public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 9b32ca9..4a652b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -73,6 +73,9 @@
 
   @Override
   public GroupDescription.Basic get(AccountGroup.UUID uuid) {
+    if (uuid == null) {
+      return null;
+    }
     GroupBackend b = backend(uuid);
     if (b == null) {
       log.warn("Unknown GroupBackend for UUID: " + uuid);
@@ -121,6 +124,9 @@
 
    @Override
    public boolean contains(AccountGroup.UUID uuid) {
+     if (uuid == null) {
+       return false;
+     }
      GroupMembership m = membership(uuid);
      if (m == null) {
        log.warn("Unknown GroupMembership for UUID: " + uuid);
@@ -134,6 +140,9 @@
       Multimap<GroupMembership, AccountGroup.UUID> lookups =
           ArrayListMultimap.create();
       for (AccountGroup.UUID uuid : uuids) {
+        if (uuid == null) {
+          continue;
+        }
         GroupMembership m = membership(uuid);
         if (m == null) {
           log.warn("Unknown GroupMembership for UUID: " + uuid);
@@ -161,6 +170,9 @@
       Multimap<GroupMembership, AccountGroup.UUID> lookups =
           ArrayListMultimap.create();
       for (AccountGroup.UUID uuid : uuids) {
+        if (uuid == null) {
+          continue;
+        }
         GroupMembership m = membership(uuid);
         if (m == null) {
           log.warn("Unknown GroupMembership for UUID: " + uuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java
new file mode 100644
index 0000000..b12e7ce
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VersionedAccountQueries.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.QueryList;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.git.VersionedMetaData;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/** Named Queries for user accounts. */
+public class VersionedAccountQueries extends VersionedMetaData {
+  private static final Logger log = LoggerFactory.getLogger(VersionedAccountQueries.class);
+
+  public static VersionedAccountQueries forUser(Account.Id id) {
+    return new VersionedAccountQueries(RefNames.refsUsers(id));
+  }
+
+  private final String ref;
+  private QueryList queryList;
+
+  private VersionedAccountQueries(String ref) {
+    this.ref = ref;
+  }
+
+  @Override
+  protected String getRefName() {
+    return ref;
+  }
+
+  public QueryList getQueryList() {
+    return queryList;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    ValidationError.Sink errors = new ValidationError.Sink() {
+      @Override
+      public void error(ValidationError error) {
+        log.error("Error parsing file " + QueryList.FILE_NAME + ": " +
+            error.getMessage());
+      }
+    };
+    queryList = QueryList.parse(readUTF8(QueryList.FILE_NAME), errors);
+  }
+
+  @Override
+  protected boolean onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    throw new UnsupportedOperationException("Cannot yet save named queries");
+  }
+}
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 06c59ee..78b3f10 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
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.api.changes.RevisionApi;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.EditInfo;
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -40,6 +41,8 @@
 import com.google.gerrit.server.change.Check;
 import com.google.gerrit.server.change.GetHashtags;
 import com.google.gerrit.server.change.GetTopic;
+import com.google.gerrit.server.change.ListChangeComments;
+import com.google.gerrit.server.change.ListChangeDrafts;
 import com.google.gerrit.server.change.PostHashtags;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.PutTopic;
@@ -56,6 +59,7 @@
 import java.io.IOException;
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 class ChangeApiImpl implements ChangeApi {
@@ -78,6 +82,8 @@
   private final Provider<ChangeJson> changeJson;
   private final PostHashtags postHashtags;
   private final GetHashtags getHashtags;
+  private final ListChangeComments listComments;
+  private final ListChangeDrafts listDrafts;
   private final Check check;
   private final ChangeEdits.Detail editDetail;
 
@@ -96,6 +102,8 @@
       Provider<ChangeJson> changeJson,
       PostHashtags postHashtags,
       GetHashtags getHashtags,
+      ListChangeComments listComments,
+      ListChangeDrafts listDrafts,
       Check check,
       ChangeEdits.Detail editDetail,
       @Assisted ChangeResource change) {
@@ -113,6 +121,8 @@
     this.changeJson = changeJson;
     this.postHashtags = postHashtags;
     this.getHashtags = getHashtags;
+    this.listComments = listComments;
+    this.listDrafts = listDrafts;
     this.check = check;
     this.editDetail = editDetail;
     this.change = change;
@@ -298,6 +308,24 @@
   }
 
   @Override
+  public Map<String, List<CommentInfo>> comments() throws RestApiException {
+    try {
+      return listComments.apply(change);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get comments", e);
+    }
+  }
+
+  @Override
+  public Map<String, List<CommentInfo>> drafts() throws RestApiException {
+    try {
+      return listDrafts.apply(change);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot get drafts", e);
+    }
+  }
+
+  @Override
   public ChangeInfo check() throws RestApiException {
     try {
       return check.apply(change).value();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 7441a60..1b420f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -42,8 +42,8 @@
 import com.google.gerrit.server.change.FileResource;
 import com.google.gerrit.server.change.Files;
 import com.google.gerrit.server.change.GetRevisionActions;
-import com.google.gerrit.server.change.ListComments;
-import com.google.gerrit.server.change.ListDraftComments;
+import com.google.gerrit.server.change.ListRevisionComments;
+import com.google.gerrit.server.change.ListRevisionDrafts;
 import com.google.gerrit.server.change.Mergeable;
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.PublishDraftPatchSet;
@@ -82,8 +82,8 @@
   private final Provider<PostReview> review;
   private final Provider<Mergeable> mergeable;
   private final FileApiImpl.Factory fileApi;
-  private final ListComments listComments;
-  private final ListDraftComments listDrafts;
+  private final ListRevisionComments listComments;
+  private final ListRevisionDrafts listDrafts;
   private final CreateDraftComment createDraft;
   private final DraftComments drafts;
   private final DraftApiImpl.Factory draftFactory;
@@ -106,8 +106,8 @@
       Provider<PostReview> review,
       Provider<Mergeable> mergeable,
       FileApiImpl.Factory fileApi,
-      ListComments listComments,
-      ListDraftComments listDrafts,
+      ListRevisionComments listComments,
+      ListRevisionDrafts listDrafts,
       CreateDraftComment createDraft,
       DraftComments drafts,
       DraftApiImpl.Factory draftFactory,
@@ -193,7 +193,7 @@
   public ChangeApi rebase(RebaseInput in) throws RestApiException {
     try {
       return changes.id(rebase.apply(revision, in)._number);
-    } catch (OrmException | EmailException e) {
+    } catch (OrmException | EmailException | IOException e) {
       throw new RestApiException("Cannot rebase ps", e);
     }
   }
@@ -298,6 +298,15 @@
   }
 
   @Override
+  public List<CommentInfo> commentsAsList() throws RestApiException {
+    try {
+      return listComments.getComments(revision);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve comments", e);
+    }
+  }
+
+  @Override
   public Map<String, List<CommentInfo>> drafts() throws RestApiException {
     try {
       return listDrafts.apply(revision);
@@ -307,6 +316,15 @@
   }
 
   @Override
+  public List<CommentInfo> draftsAsList() throws RestApiException {
+    try {
+      return listDrafts.getComments(revision);
+    } catch (OrmException e) {
+      throw new RestApiException("Cannot retrieve drafts", e);
+    }
+  }
+
+  @Override
   public DraftApi draft(String id) throws RestApiException {
     try {
       return draftFactory.create(drafts.parse(revision,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
index a5054f3..14fa7d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
@@ -19,14 +19,14 @@
 import org.eclipse.jgit.archive.Tbz2Format;
 import org.eclipse.jgit.archive.TgzFormat;
 import org.eclipse.jgit.archive.TxzFormat;
+import org.eclipse.jgit.archive.ZipFormat;
 
 public enum ArchiveFormat {
   TGZ("application/x-gzip", new TgzFormat()),
   TAR("application/x-tar", new TarFormat()),
   TBZ2("application/x-bzip2", new Tbz2Format()),
-  TXZ("application/x-xz", new TxzFormat());
-  // Zip is not supported because it may be interpreted by a Java plugin as a
-  // valid JAR file, whose code would have access to cookies on the domain.
+  TXZ("application/x-xz", new TxzFormat()),
+  ZIP("application/x-zip", new ZipFormat());
 
   private final ArchiveCommand.Format<?> format;
   private final String mimeType;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index 9bd625d..73f9cab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -140,7 +140,7 @@
     return deleteFileFactory.create(id.get());
   }
 
-  static class Create implements
+  public static class Create implements
       RestModifyView<ChangeResource, Put.Input> {
 
     interface Factory {
@@ -195,7 +195,7 @@
     }
   }
 
-  static class DeleteFile implements
+  public static class DeleteFile implements
       RestModifyView<ChangeResource, DeleteFile.Input> {
     public static class Input {
     }
@@ -407,7 +407,7 @@
    * as reverting or restoring a file to its previous contents.
    */
   @Singleton
-  static class DeleteContent implements
+  public static class DeleteContent implements
       RestModifyView<ChangeEditResource, DeleteContent.Input> {
     public static class Input {
     }
@@ -432,7 +432,7 @@
   }
 
   @Singleton
-  static class Get implements RestReadView<ChangeEditResource> {
+  public static class Get implements RestReadView<ChangeEditResource> {
     private final FileContentUtil fileContentUtil;
 
     @Inject
@@ -455,7 +455,7 @@
   }
 
   @Singleton
-  static class GetMeta implements RestReadView<ChangeEditResource> {
+  public static class GetMeta implements RestReadView<ChangeEditResource> {
     private final WebLinks webLinks;
 
     @Inject
@@ -481,8 +481,8 @@
       return r;
     }
 
-    static class FileInfo {
-      List<DiffWebLinkInfo> webLinks;
+    public static class FileInfo {
+      public List<DiffWebLinkInfo> webLinks;
     }
   }
 
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 6753582..28614f7 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
@@ -19,6 +19,7 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CHANGE_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CHECK;
+import static com.google.gerrit.extensions.client.ListChangesOption.COMMIT_FOOTERS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_ACTIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
 import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_FILES;
@@ -31,12 +32,14 @@
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
 import static com.google.gerrit.extensions.client.ListChangesOption.WEB_LINKS;
+import static com.google.gerrit.server.CommonConverters.toGitPerson;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Function;
 import com.google.common.base.Joiner;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
 import com.google.common.collect.FluentIterable;
 import com.google.common.collect.HashBasedTable;
 import com.google.common.collect.HashMultimap;
@@ -65,7 +68,6 @@
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.FetchInfo;
-import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.ProblemInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
@@ -80,10 +82,7 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ChangeMessagesUtil;
@@ -92,12 +91,13 @@
 import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.WebLinks;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LabelNormalizer;
+import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
@@ -106,10 +106,16 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -129,9 +135,11 @@
   private final LabelNormalizer labelNormalizer;
   private final Provider<CurrentUser> userProvider;
   private final AnonymousUser anonymous;
+  private final GitRepositoryManager repoManager;
+  private final ProjectCache projectCache;
+  private final MergeUtil.Factory mergeUtilFactory;
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeData.Factory changeDataFactory;
-  private final PatchSetInfoFactory patchSetInfoFactory;
   private final FileInfoJson fileInfoJson;
   private final AccountLoader.Factory accountLoaderFactory;
   private final DynamicMap<DownloadScheme> downloadSchemes;
@@ -153,9 +161,11 @@
       LabelNormalizer ln,
       Provider<CurrentUser> user,
       AnonymousUser au,
+      GitRepositoryManager repoManager,
+      ProjectCache projectCache,
+      MergeUtil.Factory mergeUtilFactory,
       IdentifiedUser.GenericFactory uf,
       ChangeData.Factory cdf,
-      PatchSetInfoFactory psi,
       FileInfoJson fileInfoJson,
       AccountLoader.Factory ailf,
       DynamicMap<DownloadScheme> downloadSchemes,
@@ -170,9 +180,11 @@
     this.labelNormalizer = ln;
     this.userProvider = user;
     this.anonymous = au;
-    this.userFactory = uf;
     this.changeDataFactory = cdf;
-    this.patchSetInfoFactory = psi;
+    this.repoManager = repoManager;
+    this.userFactory = uf;
+    this.projectCache = projectCache;
+    this.mergeUtilFactory = mergeUtilFactory;
     this.fileInfoJson = fileInfoJson;
     this.accountLoaderFactory = ailf;
     this.downloadSchemes = downloadSchemes;
@@ -237,9 +249,11 @@
       ChangeInfo res = toChangeInfo(cd, reviewed, limitToPsId);
       accountLoader.fill();
       return res;
-    } catch (OrmException | RuntimeException e) {
+    } catch (PatchListNotAvailableException | OrmException | IOException
+        | RuntimeException e) {
       if (!has(CHECK)) {
-        throw e;
+        Throwables.propagateIfPossible(e, OrmException.class);
+        throw new OrmException(e);
       }
       return checkOnly(cd);
     }
@@ -263,7 +277,7 @@
     ChangeData.ensureChangeLoaded(all);
     if (has(ALL_REVISIONS)) {
       ChangeData.ensureAllPatchSetsLoaded(all);
-    } else {
+    } else if (has(CURRENT_REVISION) || has(MESSAGES)) {
       ChangeData.ensureCurrentPatchSetLoaded(all);
     }
     Set<Change.Id> reviewed = Sets.newHashSet();
@@ -297,7 +311,8 @@
       if (i == null) {
         try {
           i = toChangeInfo(cd, reviewed, Optional.<PatchSet.Id> absent());
-        } catch (OrmException | RuntimeException e) {
+        } catch (PatchListNotAvailableException | OrmException | IOException
+            | RuntimeException e) {
           if (has(CHECK)) {
             i = checkOnly(cd);
           } else {
@@ -340,7 +355,8 @@
   }
 
   private ChangeInfo toChangeInfo(ChangeData cd, Set<Change.Id> reviewed,
-      Optional<PatchSet.Id> limitToPsId) throws OrmException {
+      Optional<PatchSet.Id> limitToPsId)
+      throws PatchListNotAvailableException, OrmException, IOException {
     ChangeInfo out = new ChangeInfo();
 
     if (has(CHECK)) {
@@ -395,16 +411,23 @@
       out.removableReviewers = removableReviewers(ctl, out.labels.values());
     }
 
-    Map<PatchSet.Id, PatchSet> src = loadPatchSets(cd, limitToPsId);
-    if (has(MESSAGES)) {
+    boolean needMessages = has(MESSAGES);
+    boolean needRevisions = has(ALL_REVISIONS)
+        || has(CURRENT_REVISION)
+        || limitToPsId.isPresent();
+    Map<PatchSet.Id, PatchSet> src;
+    if (needMessages || needRevisions) {
+      src = loadPatchSets(cd, limitToPsId);
+    } else {
+      src = null;
+    }
+    if (needMessages) {
       out.messages = messages(ctl, cd, src);
     }
     finish(out);
 
-    if (has(ALL_REVISIONS)
-        || has(CURRENT_REVISION)
-        || limitToPsId.isPresent()) {
-      out.revisions = revisions(ctl, cd, src);
+    if (needRevisions) {
+      out.revisions = revisions(ctl, src);
       if (out.revisions != null) {
         for (Map.Entry<String, RevisionInfo> entry : out.revisions.entrySet()) {
           if (entry.getValue().isCurrent) {
@@ -426,14 +449,10 @@
     if (cd.getSubmitRecords() != null) {
       return cd.getSubmitRecords();
     }
-    PatchSet ps = cd.currentPatchSet();
-    if (ps == null) {
-      return ImmutableList.of();
-    }
-    cd.setSubmitRecords(new SubmitRuleEvaluator(cd).setPatchSet(ps)
+    cd.setSubmitRecords(new SubmitRuleEvaluator(cd)
         .setFastEvalLabels(true)
         .setAllowDraft(true)
-        .canSubmit());
+        .evaluate());
     return cd.getSubmitRecords();
   }
 
@@ -591,8 +610,14 @@
       LabelTypes labelTypes, boolean standard, boolean detailed)
       throws OrmException {
     Set<Account.Id> allUsers = Sets.newHashSet();
-    for (PatchSetApproval psa : cd.approvals().values()) {
-      allUsers.add(psa.getAccountId());
+    if (detailed) {
+      // Users expect to see all reviewers on closed changes, even if they
+      // didn't vote on the latest patch set. If we don't need detailed labels,
+      // we aren't including 0 votes for all users below, so we can just look at
+      // the latest patch set (in the next loop).
+      for (PatchSetApproval psa : cd.approvals().values()) {
+        allUsers.add(psa.getAccountId());
+      }
     }
 
     // We can only approximately reconstruct what the submit rule evaluator
@@ -600,6 +625,7 @@
     Set<String> labelNames = Sets.newHashSet();
     Multimap<Account.Id, PatchSetApproval> current = HashMultimap.create();
     for (PatchSetApproval a : cd.currentApprovals()) {
+      allUsers.add(a.getAccountId());
       LabelType type = labelTypes.byLabel(a.getLabelId());
       if (type != null) {
         labelNames.add(type.getName());
@@ -819,14 +845,15 @@
     return false;
   }
 
-  private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd,
-      Map<PatchSet.Id, PatchSet> map) throws OrmException {
+  private Map<String, RevisionInfo> revisions(ChangeControl ctl,
+      Map<PatchSet.Id, PatchSet> map)
+      throws PatchListNotAvailableException, OrmException, IOException {
     Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
     for (PatchSet in : map.values()) {
       if ((has(ALL_REVISIONS)
-          || in.getId().equals(cd.change().currentPatchSetId()))
+          || in.getId().equals(ctl.getChange().currentPatchSetId()))
           && ctl.isPatchVisible(in, db.get())) {
-        res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in));
+        res.put(in.getRevision().get(), toRevisionInfo(ctl, in));
       }
     }
     return res;
@@ -836,11 +863,11 @@
       Optional<PatchSet.Id> limitToPsId) throws OrmException {
     Collection<PatchSet> src;
     if (has(ALL_REVISIONS) || has(MESSAGES)) {
-      src = cd.patches();
+      src = cd.patchSets();
     } else {
       PatchSet ps;
       if (limitToPsId.isPresent()) {
-        ps = cd.patch(limitToPsId.get());
+        ps = cd.patchSet(limitToPsId.get());
         if (ps == null) {
           throw new OrmException("missing patch set " + limitToPsId.get());
         }
@@ -860,10 +887,11 @@
     return map;
   }
 
-  private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd,
-      PatchSet in) throws OrmException {
+  private RevisionInfo toRevisionInfo(ChangeControl ctl, PatchSet in)
+      throws PatchListNotAvailableException, OrmException, IOException {
+    Change c = ctl.getChange();
     RevisionInfo out = new RevisionInfo();
-    out.isCurrent = in.getId().equals(cd.change().currentPatchSetId());
+    out.isCurrent = in.getId().equals(c.currentPatchSetId());
     out._number = in.getId().get();
     out.ref = in.getRefName();
     out.created = in.getCreatedOn();
@@ -871,21 +899,30 @@
     out.draft = in.isDraft() ? true : null;
     out.fetch = makeFetchMap(ctl, in);
 
-    if (has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT))) {
-      try {
-        out.commit = toCommit(in, cd.change().getProject(), has(WEB_LINKS));
-      } catch (PatchSetInfoNotAvailableException e) {
-        throw new OrmException(e);
+    boolean setCommit = has(ALL_COMMITS)
+        || (out.isCurrent && has(CURRENT_COMMIT));
+    boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
+    if (setCommit || addFooters) {
+      Project.NameKey project = c.getProject();
+      try (Repository repo = repoManager.openRepository(project);
+          RevWalk rw = new RevWalk(repo)) {
+        String rev = in.getRevision().get();
+        RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
+        rw.parseBody(commit);
+        if (setCommit) {
+          out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS));
+        }
+        if (addFooters) {
+          out.commitWithFooters = mergeUtilFactory
+              .create(projectCache.get(project))
+              .createCherryPickCommitMessage(commit, ctl, in.getId());
+        }
       }
     }
 
     if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
-      try {
-        out.files = fileInfoJson.toFileInfoMap(cd.change(), in);
-        out.files.remove(Patch.COMMIT_MSG);
-      } catch (PatchListNotAvailableException e) {
-        throw new OrmException(e);
-      }
+      out.files = fileInfoJson.toFileInfoMap(c, in);
+      out.files.remove(Patch.COMMIT_MSG);
     }
 
     if ((out.isCurrent || (out.draft != null && out.draft))
@@ -908,34 +945,35 @@
     return out;
   }
 
-  CommitInfo toCommit(PatchSet in, Project.NameKey project, boolean addLinks)
-      throws PatchSetInfoNotAvailableException {
-    PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
-    CommitInfo commit = new CommitInfo();
-    commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
-    commit.author = toGitPerson(info.getAuthor());
-    commit.committer = toGitPerson(info.getCommitter());
-    commit.subject = info.getSubject();
-    commit.message = info.getMessage();
+  CommitInfo toCommit(ChangeControl ctl, RevWalk rw, RevCommit commit,
+      boolean addLinks) throws IOException {
+    Project.NameKey project = ctl.getChange().getProject();
+    CommitInfo info = new CommitInfo();
+    info.parents = new ArrayList<>(commit.getParentCount());
+    info.author = toGitPerson(commit.getAuthorIdent());
+    info.committer = toGitPerson(commit.getCommitterIdent());
+    info.subject = commit.getShortMessage();
+    info.message = commit.getFullMessage();
 
     if (addLinks) {
       FluentIterable<WebLinkInfo> links =
-          webLinks.getPatchSetLinks(project, in.getRevision().get());
-      commit.webLinks = links.isEmpty() ? null : links.toList();
+          webLinks.getPatchSetLinks(project, commit.name());
+      info.webLinks = links.isEmpty() ? null : links.toList();
     }
 
-    for (ParentInfo parent : info.getParents()) {
+    for (RevCommit parent : commit.getParents()) {
+      rw.parseBody(parent);
       CommitInfo i = new CommitInfo();
-      i.commit = parent.id.get();
-      i.subject = parent.shortMessage;
+      i.commit = parent.name();
+      i.subject = parent.getShortMessage();
       if (addLinks) {
         FluentIterable<WebLinkInfo> parentLinks =
-            webLinks.getPatchSetLinks(project, parent.id.get());
+            webLinks.getPatchSetLinks(project, parent.name());
         i.webLinks = parentLinks.isEmpty() ? null : parentLinks.toList();
       }
-      commit.parents.add(i);
+      info.parents.add(i);
     }
-    return commit;
+    return info;
   }
 
   private Map<String, FetchInfo> makeFetchMap(ChangeControl ctl, PatchSet in)
@@ -991,15 +1029,6 @@
     fetchInfo.commands.put(commandName, c);
   }
 
-  private static GitPerson toGitPerson(UserIdentity committer) {
-    GitPerson p = new GitPerson();
-    p.name = committer.getName();
-    p.email = committer.getEmail();
-    p.date = committer.getDate();
-    p.tz = committer.getTimeZone();
-    return p;
-  }
-
   static void finish(ChangeInfo info) {
     info.id = Joiner.on('~').join(
         Url.encode(info.project),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
index d22d6ff..6e6f6fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKind.java
@@ -26,5 +26,5 @@
   NO_CODE_CHANGE,
 
   /** Same tree, parent tree, same commit message. */
-  NO_CHANGE;
+  NO_CHANGE
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 23039aa..8fdd445 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -317,7 +317,7 @@
         repo = repoManager.openRepository(change.getProject());
 
         ChangeData cd = changeDataFactory.create(db, change);
-        Collection<PatchSet> patchSetCollection = cd.patches();
+        Collection<PatchSet> patchSetCollection = cd.patchSets();
         PatchSet priorPs = patch;
         for (PatchSet ps : patchSetCollection) {
           if (ps.getId().get() < patch.getId().get() &&
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index b386894..b8e1178 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -182,9 +183,13 @@
       } else {
         // Change key not found on destination branch. We can create a new
         // change.
+        String newTopic = null;
+        if (!Strings.isNullOrEmpty(change.getTopic())) {
+          newTopic = change.getTopic() + "-" + newDest.getShortName();
+        }
         Change newChange = createNewChange(git, revWalk, changeKey, project,
             destRef, cherryPickCommit, refControl,
-            identifiedUser, change.getTopic());
+            identifiedUser, newTopic);
 
         addMessageToSourceChange(change, patch.getId(), destinationBranch,
             cherryPickCommit, identifiedUser, refControl);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
index 4a52fc5..b155b84 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.gerrit.server.PatchLineCommentsUtil.COMMENT_INFO_ORDER;
 
+import com.google.common.base.Function;
 import com.google.common.base.Strings;
+import com.google.common.collect.FluentIterable;
 import com.google.gerrit.extensions.client.Comment.Range;
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
@@ -26,45 +28,51 @@
 import com.google.gerrit.server.account.AccountLoader;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 
-@Singleton
 class CommentJson {
 
   private final AccountLoader.Factory accountLoaderFactory;
 
+  private boolean fillAccounts = true;
+  private boolean fillPatchSet;
+
   @Inject
   CommentJson(AccountLoader.Factory accountLoaderFactory) {
     this.accountLoaderFactory = accountLoaderFactory;
   }
 
-  CommentInfo format(PatchLineComment c) throws OrmException {
-    return format(c, true);
+  CommentJson setFillAccounts(boolean fillAccounts) {
+    this.fillAccounts = fillAccounts;
+    return this;
   }
 
-  CommentInfo format(PatchLineComment c, boolean fill) throws OrmException {
+  CommentJson setFillPatchSet(boolean fillPatchSet) {
+    this.fillPatchSet = fillPatchSet;
+    return this;
+  }
+
+  CommentInfo format(PatchLineComment c) throws OrmException {
     AccountLoader loader = null;
-    if (fill) {
+    if (fillAccounts) {
       loader = accountLoaderFactory.create(true);
     }
     CommentInfo commentInfo = toCommentInfo(c, loader);
-    if (fill) {
+    if (fillAccounts) {
       loader.fill();
     }
     return commentInfo;
   }
 
-  Map<String, List<CommentInfo>> format(Iterable<PatchLineComment> l,
-      boolean fill) throws OrmException {
+  Map<String, List<CommentInfo>> format(Iterable<PatchLineComment> l)
+      throws OrmException {
     Map<String, List<CommentInfo>> out = new TreeMap<>();
-    AccountLoader accountLoader = fill
+    AccountLoader accountLoader = fillAccounts
         ? accountLoaderFactory.create(true)
         : null;
 
@@ -80,20 +88,7 @@
     }
 
     for (List<CommentInfo> list : out.values()) {
-      Collections.sort(list, new Comparator<CommentInfo>() {
-        @Override
-        public int compare(CommentInfo a, CommentInfo b) {
-          int c = firstNonNull(a.side, Side.REVISION).ordinal()
-                - firstNonNull(b.side, Side.REVISION).ordinal();
-          if (c == 0) {
-            c = firstNonNull(a.line, 0) - firstNonNull(b.line, 0);
-          }
-          if (c == 0) {
-            c = a.id.compareTo(b.id);
-          }
-          return c;
-        }
-      });
+      Collections.sort(list, COMMENT_INFO_ORDER);
     }
 
     if (accountLoader != null) {
@@ -103,8 +98,32 @@
     return out;
   }
 
+  List<CommentInfo> formatAsList(Iterable<PatchLineComment> l)
+      throws OrmException {
+    final AccountLoader accountLoader = fillAccounts
+        ? accountLoaderFactory.create(true)
+        : null;
+    List<CommentInfo> out = FluentIterable
+        .from(l)
+        .transform(new Function<PatchLineComment, CommentInfo>() {
+          @Override
+          public CommentInfo apply(PatchLineComment c) {
+            return toCommentInfo(c, accountLoader);
+          }
+        }).toSortedList(COMMENT_INFO_ORDER);
+
+    if (accountLoader != null) {
+      accountLoader.fill();
+    }
+
+    return out;
+  }
+
   private CommentInfo toCommentInfo(PatchLineComment c, AccountLoader loader) {
     CommentInfo r = new CommentInfo();
+    if (fillPatchSet) {
+      r.patchSet = c.getKey().getParentKey().getParentKey().get();
+    }
     r.id = Url.encode(c.getKey().get());
     r.path = c.getKey().getParentKey().getFileName();
     if (c.getSide() == 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
index f25da7c..8f78f0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Comments.java
@@ -31,13 +31,13 @@
 @Singleton
 public class Comments implements ChildCollection<RevisionResource, CommentResource> {
   private final DynamicMap<RestView<CommentResource>> views;
-  private final ListComments list;
+  private final ListRevisionComments list;
   private final Provider<ReviewDb> dbProvider;
   private final PatchLineCommentsUtil plcUtil;
 
   @Inject
   Comments(DynamicMap<RestView<CommentResource>> views,
-      ListComments list, Provider<ReviewDb> dbProvider,
+      ListRevisionComments list, Provider<ReviewDb> dbProvider,
       PatchLineCommentsUtil plcUtil) {
     this.views = views;
     this.list = list;
@@ -51,7 +51,7 @@
   }
 
   @Override
-  public RestView<RevisionResource> list() {
+  public ListRevisionComments list() {
     return list;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 264a3f7..87a1ca6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER;
+import static com.google.gerrit.server.ChangeUtil.TO_PS_ID;
+
 import com.google.auto.value.AutoValue;
-import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Ordering;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.FixInput;
 import com.google.gerrit.extensions.common.ProblemInfo;
@@ -210,17 +211,6 @@
     }
   }
 
-  private static final Function<PatchSet, Integer> TO_PS_ID =
-      new Function<PatchSet, Integer>() {
-        @Override
-        public Integer apply(PatchSet in) {
-          return in.getId().get();
-        }
-      };
-
-  private static final Ordering<PatchSet> PS_ID_ORDER = Ordering.natural()
-    .onResultOf(TO_PS_ID);
-
   private boolean checkPatchSets() {
     List<PatchSet> all;
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
index 36b9692..a503721 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -45,14 +45,14 @@
 public class CreateDraftComment implements RestModifyView<RevisionResource, DraftInput> {
   private final Provider<ReviewDb> db;
   private final ChangeUpdate.Factory updateFactory;
-  private final CommentJson commentJson;
+  private final Provider<CommentJson> commentJson;
   private final PatchLineCommentsUtil plcUtil;
   private final PatchListCache patchListCache;
 
   @Inject
   CreateDraftComment(Provider<ReviewDb> db,
       ChangeUpdate.Factory updateFactory,
-      CommentJson commentJson,
+      Provider<CommentJson> commentJson,
       PatchLineCommentsUtil plcUtil,
       PatchListCache patchListCache) {
     this.db = db;
@@ -93,6 +93,6 @@
     setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
     plcUtil.insertComments(db.get(), update, Collections.singleton(c));
     update.commit();
-    return Response.created(commentJson.format(c, false));
+    return Response.created(commentJson.get().setFillAccounts(false).format(c));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
index fdb181b..acb50ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftComments.java
@@ -33,14 +33,14 @@
 public class DraftComments implements ChildCollection<RevisionResource, DraftCommentResource> {
   private final DynamicMap<RestView<DraftCommentResource>> views;
   private final Provider<CurrentUser> user;
-  private final ListDraftComments list;
+  private final ListRevisionDrafts list;
   private final Provider<ReviewDb> dbProvider;
   private final PatchLineCommentsUtil plcUtil;
 
   @Inject
   DraftComments(DynamicMap<RestView<DraftCommentResource>> views,
       Provider<CurrentUser> user,
-      ListDraftComments list,
+      ListRevisionDrafts list,
       Provider<ReviewDb> dbProvider,
       PatchLineCommentsUtil plcUtil) {
     this.views = views;
@@ -56,7 +56,7 @@
   }
 
   @Override
-  public RestView<RevisionResource> list() throws AuthException {
+  public ListRevisionDrafts list() throws AuthException {
     checkIdentifiedUser();
     return list;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
index d75b6c6..122156b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.collect.Lists;
+import static com.google.gerrit.server.PatchLineCommentsUtil.PLC_ORDER;
+
 import com.google.gerrit.extensions.api.changes.ReviewInput.NotifyHandling;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -24,7 +25,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.git.EmailReviewCommentsExecutor;
-import com.google.gerrit.server.git.WorkQueue.Executor;
 import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.util.RequestContext;
@@ -40,9 +40,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
 
 public class EmailReviewComments implements Runnable, RequestContext {
   private static final Logger log = LoggerFactory.getLogger(EmailReviewComments.class);
@@ -57,7 +56,7 @@
         List<PatchLineComment> comments);
   }
 
-  private final Executor sendEmailsExecutor;
+  private final ExecutorService sendEmailsExecutor;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final CommentSender.Factory commentSenderFactory;
   private final SchemaFactory<ReviewDb> schemaFactory;
@@ -73,7 +72,7 @@
 
   @Inject
   EmailReviewComments (
-      @EmailReviewCommentsExecutor final Executor executor,
+      @EmailReviewCommentsExecutor ExecutorService executor,
       PatchSetInfoFactory patchSetInfoFactory,
       CommentSender.Factory commentSenderFactory,
       SchemaFactory<ReviewDb> schemaFactory,
@@ -94,7 +93,7 @@
     this.patchSet = patchSet;
     this.authorId = authorId;
     this.message = message;
-    this.comments = comments;
+    this.comments = PLC_ORDER.sortedCopy(comments);
   }
 
   void sendAsync() {
@@ -103,31 +102,8 @@
 
   @Override
   public void run() {
+    RequestContext old = requestContext.setContext(this);
     try {
-      requestContext.setContext(this);
-
-      comments = Lists.newArrayList(comments);
-      Collections.sort(comments, new Comparator<PatchLineComment>() {
-        @Override
-        public int compare(PatchLineComment a, PatchLineComment b) {
-          int cmp = path(a).compareTo(path(b));
-          if (cmp != 0) {
-            return cmp;
-          }
-
-          // 0 is ancestor, 1 is revision. Sort ancestor first.
-          cmp = a.getSide() - b.getSide();
-          if (cmp != 0) {
-            return cmp;
-          }
-
-          return a.getLine() - b.getLine();
-        }
-
-        private String path(PatchLineComment c) {
-          return c.getKey().getParentKey().getFileName();
-        }
-      });
 
       CommentSender cm = commentSenderFactory.create(notify, change.getId());
       cm.setFrom(authorId);
@@ -138,7 +114,7 @@
     } catch (Exception e) {
       log.error("Cannot email comments for " + patchSet.getId(), e);
     } finally {
-      requestContext.setContext(null);
+      requestContext.setContext(old);
       if (db != null) {
         db.close();
         db = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
index 1662237..ca47fb9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileResource.java
@@ -44,7 +44,7 @@
     return rev.getAccountId();
   }
 
-  RevisionResource getRevision() {
+  public RevisionResource getRevision() {
     return rev;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
index 913f69e..ccc7645 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
@@ -18,16 +18,15 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.api.ArchiveCommand;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -36,11 +35,7 @@
 
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
-import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -51,33 +46,25 @@
     final Set<ArchiveFormat> allowed;
 
     @Inject
-    AllowedFormats(@GerritServerConfig Config cfg) {
-      Collection<ArchiveFormat> enabled;
-      String v = cfg.getString("download", null, "archive");
-      if (v == null) {
-        enabled = Arrays.asList(ArchiveFormat.values());
-      } else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
-        enabled = Collections.emptyList();
-      } else {
-        enabled = ConfigUtil.getEnumList(cfg,
-            "download", null, "archive",
-            ArchiveFormat.TGZ);
-      }
-
+    AllowedFormats(DownloadConfig cfg) {
       Map<String, ArchiveFormat> exts = new HashMap<>();
-      for (ArchiveFormat format : enabled) {
+      for (ArchiveFormat format : cfg.getArchiveFormats()) {
         for (String ext : format.getSuffixes()) {
           exts.put(ext, format);
         }
         exts.put(format.name().toLowerCase(), format);
       }
       extensions = ImmutableMap.copyOf(exts);
-      allowed = Collections.unmodifiableSet(new LinkedHashSet<>(enabled));
+      allowed = cfg.getArchiveFormats();
     }
 
     public Set<ArchiveFormat> getAllowed() {
       return allowed;
     }
+
+    public ImmutableMap<String, ArchiveFormat> getExtensions() {
+      return extensions;
+    }
   }
 
   private final GitRepositoryManager repoManager;
@@ -93,8 +80,8 @@
   }
 
   @Override
-  public BinaryResult apply(RevisionResource rsrc)
-      throws BadRequestException, IOException {
+  public BinaryResult apply(RevisionResource rsrc) throws BadRequestException,
+      IOException, MethodNotAllowedException {
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
     }
@@ -102,6 +89,9 @@
     if (f == null) {
       throw new BadRequestException("unknown archive format");
     }
+    if (f == ArchiveFormat.ZIP) {
+      throw new MethodNotAllowedException("zip format is disabled");
+    }
     boolean close = true;
     final Repository repo = repoManager
         .openRepository(rsrc.getControl().getProject().getNameKey());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
index ea84f50..d87c7eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetComment.java
@@ -18,20 +18,21 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetComment implements RestReadView<CommentResource> {
 
-  private final CommentJson commentJson;
+  private final Provider<CommentJson> commentJson;
 
   @Inject
-  GetComment(CommentJson commentJson) {
+  GetComment(Provider<CommentJson> commentJson) {
     this.commentJson = commentJson;
   }
 
   @Override
   public CommentInfo apply(CommentResource rsrc) throws OrmException {
-    return commentJson.format(rsrc.getComment());
+    return commentJson.get().format(rsrc.getComment());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
index 296a262..0f0a6a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -18,38 +18,47 @@
 import com.google.gerrit.extensions.restapi.CacheControl;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.kohsuke.args4j.Option;
 
+import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 
 public class GetCommit implements RestReadView<RevisionResource> {
+  private final GitRepositoryManager repoManager;
   private final ChangeJson json;
 
   @Option(name = "--links", usage = "Add weblinks")
   private boolean addLinks;
 
   @Inject
-  GetCommit(ChangeJson json) {
+  GetCommit(GitRepositoryManager repoManager,
+      ChangeJson json) {
+    this.repoManager = repoManager;
     this.json = json;
   }
 
   @Override
-  public Response<CommitInfo> apply(RevisionResource resource)
-      throws OrmException {
-    try {
-      Response<CommitInfo> r =
-          Response.ok(json.toCommit(resource.getPatchSet(), resource
-              .getChange().getProject(), addLinks));
-      if (resource.isCacheable()) {
+  public Response<CommitInfo> apply(RevisionResource rsrc) throws IOException {
+    Project.NameKey p = rsrc.getChange().getProject();
+    try (Repository repo = repoManager.openRepository(p);
+        RevWalk rw = new RevWalk(repo)) {
+      String rev = rsrc.getPatchSet().getRevision().get();
+      RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
+      rw.parseBody(commit);
+      Response<CommitInfo> r = Response.ok(
+          json.toCommit(rsrc.getControl(), rw, commit, addLinks));
+      if (rsrc.isCacheable()) {
         r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS));
       }
       return r;
-    } catch (PatchSetInfoNotAvailableException e) {
-      throw new OrmException(e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index 8e3a5d1..eef0533 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -201,6 +201,7 @@
           result.metaA.lines = ps.getA().size();
           result.metaA.webLinks =
               getFileWebLinks(state.getProject(), revA, result.metaA.name);
+          result.metaA.commitId = content.commitIdA;
         }
 
         if (ps.getDisplayMethodB() != DisplayMethod.NONE) {
@@ -211,6 +212,7 @@
           result.metaB.lines = ps.getB().size();
           result.metaB.webLinks =
               getFileWebLinks(state.getProject(), revB, result.metaB.name);
+          result.metaB.commitId = content.commitIdB;
         }
 
         if (intraline) {
@@ -264,6 +266,8 @@
     final SparseFileContent fileA;
     final SparseFileContent fileB;
     final boolean ignoreWS;
+    final String commitIdA;
+    final String commitIdB;
 
     int nextA;
     int nextB;
@@ -273,6 +277,8 @@
       fileA = ps.getA();
       fileB = ps.getB();
       ignoreWS = ps.isIgnoreWhitespace();
+      commitIdA = ps.getCommitIdA();
+      commitIdB = ps.getCommitIdB();
     }
 
     void addCommon(int end) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
index a13ecdf..22f90c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraftComment.java
@@ -18,20 +18,21 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 @Singleton
 public class GetDraftComment implements RestReadView<DraftCommentResource> {
 
-  private final CommentJson commentJson;
+  private final Provider<CommentJson> commentJson;
 
   @Inject
-  GetDraftComment(CommentJson commentJson) {
+  GetDraftComment(Provider<CommentJson> commentJson) {
     this.commentJson = commentJson;
   }
 
   @Override
   public CommentInfo apply(DraftCommentResource rsrc) throws OrmException {
-    return commentJson.format(rsrc.getComment());
+    return commentJson.get().format(rsrc.getComment());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 6cdae44..7f49192 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -144,7 +144,7 @@
     Map<PatchSet.Id, PatchSet> r =
         Maps.newHashMapWithExpectedSize(cds.size() * 2);
     for (ChangeData cd : cds) {
-      for (PatchSet p : cd.patches()) {
+      for (PatchSet p : cd.patchSets()) {
         r.put(p.getId(), p);
       }
     }
@@ -277,6 +277,9 @@
     public Integer _revisionNumber;
     public Integer _currentRevisionNumber;
 
+    public ChangeAndCommit() {
+    }
+
     ChangeAndCommit(@Nullable Change change, @Nullable PatchSet ps, RevCommit c) {
       if (change != null) {
         changeId = change.getKey().get();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
new file mode 100644
index 0000000..97befa0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeComments.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class ListChangeComments implements RestReadView<ChangeResource> {
+  private final Provider<ReviewDb> db;
+  private final ChangeData.Factory changeDataFactory;
+  private final Provider<CommentJson> commentJson;
+  private final PatchLineCommentsUtil plcUtil;
+
+  @Inject
+  ListChangeComments(Provider<ReviewDb> db,
+      ChangeData.Factory changeDataFactory,
+      Provider<CommentJson> commentJson,
+      PatchLineCommentsUtil plcUtil) {
+    this.db = db;
+    this.changeDataFactory = changeDataFactory;
+    this.commentJson = commentJson;
+    this.plcUtil = plcUtil;
+  }
+
+  @Override
+  public Map<String, List<CommentInfo>> apply(
+      ChangeResource rsrc) throws AuthException, OrmException {
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
+    return commentJson.get()
+        .setFillAccounts(true)
+        .setFillPatchSet(true)
+        .format(plcUtil.publishedByChange(db.get(), cd.notes()));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
new file mode 100644
index 0000000..2b5d7d9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListChangeDrafts.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.util.List;
+import java.util.Map;
+
+@Singleton
+public class ListChangeDrafts implements RestReadView<ChangeResource> {
+  private final Provider<ReviewDb> db;
+  private final ChangeData.Factory changeDataFactory;
+  private final Provider<CommentJson> commentJson;
+  private final PatchLineCommentsUtil plcUtil;
+
+  @Inject
+  ListChangeDrafts(Provider<ReviewDb> db,
+      ChangeData.Factory changeDataFactory,
+      Provider<CommentJson> commentJson,
+      PatchLineCommentsUtil plcUtil) {
+    this.db = db;
+    this.changeDataFactory = changeDataFactory;
+    this.commentJson = commentJson;
+    this.plcUtil = plcUtil;
+  }
+
+  @Override
+  public Map<String, List<CommentInfo>> apply(
+      ChangeResource rsrc) throws AuthException, OrmException {
+    if (!rsrc.getControl().getCurrentUser().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
+    IdentifiedUser user = (IdentifiedUser) rsrc.getControl().getCurrentUser();
+    ChangeData cd = changeDataFactory.create(db.get(), rsrc.getControl());
+    List<PatchLineComment> drafts =
+        plcUtil.draftByChangeAuthor(db.get(), cd.notes(), user.getAccountId());
+    return commentJson.get()
+        .setFillAccounts(false)
+        .setFillPatchSet(true)
+        .format(drafts);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java
similarity index 90%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java
index b50e243..2392781 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionComments.java
@@ -24,10 +24,10 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class ListComments extends ListDraftComments {
+public class ListRevisionComments extends ListRevisionDrafts {
   @Inject
-  ListComments(Provider<ReviewDb> db,
-      CommentJson commentJson,
+  ListRevisionComments(Provider<ReviewDb> db,
+      Provider<CommentJson> commentJson,
       PatchLineCommentsUtil plcUtil) {
     super(db, commentJson, plcUtil);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDraftComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java
similarity index 76%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/ListDraftComments.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java
index 3375cba..ef12b2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDraftComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionDrafts.java
@@ -28,14 +28,14 @@
 import java.util.Map;
 
 @Singleton
-public class ListDraftComments implements RestReadView<RevisionResource> {
+public class ListRevisionDrafts implements RestReadView<RevisionResource> {
   protected final Provider<ReviewDb> db;
-  protected CommentJson commentJson;
+  protected final Provider<CommentJson> commentJson;
   protected final PatchLineCommentsUtil plcUtil;
 
   @Inject
-  ListDraftComments(Provider<ReviewDb> db,
-      CommentJson commentJson,
+  ListRevisionDrafts(Provider<ReviewDb> db,
+      Provider<CommentJson> commentJson,
       PatchLineCommentsUtil plcUtil) {
     this.db = db;
     this.commentJson = commentJson;
@@ -55,6 +55,15 @@
   @Override
   public Map<String, List<CommentInfo>> apply(RevisionResource rsrc)
       throws OrmException {
-    return commentJson.format(listComments(rsrc), includeAuthorInfo());
+    return commentJson.get()
+        .setFillAccounts(includeAuthorInfo())
+        .format(listComments(rsrc));
+  }
+
+  public List<CommentInfo> getComments(RevisionResource rsrc)
+      throws OrmException {
+    return commentJson.get()
+        .setFillAccounts(includeAuthorInfo())
+        .formatAsList(listComments(rsrc));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index d0e4c99..b73af931 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -52,6 +52,8 @@
     get(CHANGE_KIND, "topic").to(GetTopic.class);
     get(CHANGE_KIND, "in").to(IncludedIn.class);
     get(CHANGE_KIND, "hashtags").to(GetHashtags.class);
+    get(CHANGE_KIND, "comments").to(ListChangeComments.class);
+    get(CHANGE_KIND, "drafts").to(ListChangeDrafts.class);
     get(CHANGE_KIND, "check").to(Check.class);
     post(CHANGE_KIND, "check").to(Check.class);
     put(CHANGE_KIND, "topic").to(PutTopic.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
index 567241a..62520f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -29,7 +30,8 @@
 import java.util.Set;
 
 @Singleton
-public class PostHashtags implements RestModifyView<ChangeResource, HashtagsInput> {
+public class PostHashtags implements RestModifyView<ChangeResource, HashtagsInput>,
+    UiAction<ChangeResource>{
   private HashtagsUtil hashtagsUtil;
 
   @Inject
@@ -51,4 +53,11 @@
       throw new ResourceConflictException(e.getMessage());
     }
   }
-}
+
+  @Override
+  public UiAction.Description getDescription(ChangeResource resource) {
+    return new UiAction.Description()
+      .setLabel("Edit Hashtags")
+      .setVisible(resource.getControl().canEditHashtags());
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 436c5e4..e10077a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -345,7 +345,11 @@
 
     Map<String, PatchLineComment> drafts = Collections.emptyMap();
     if (!in.isEmpty() || draftsHandling != DraftHandling.KEEP) {
-      drafts = scanDraftComments(rsrc);
+      if (draftsHandling == DraftHandling.PUBLISH_ALL_REVISIONS) {
+        drafts = changeDrafts(rsrc);
+      } else {
+        drafts = patchSetDrafts(rsrc);
+      }
     }
 
     List<PatchLineComment> del = Lists.newArrayList();
@@ -370,8 +374,7 @@
         e.setStatus(PatchLineComment.Status.PUBLISHED);
         e.setWrittenOn(timestamp);
         e.setSide(c.side == Side.PARENT ? (short) 0 : (short) 1);
-        setCommentRevId(e, patchListCache, rsrc.getChange(),
-            rsrc.getPatchSet());
+        setCommentRevId(e, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
         e.setMessage(c.message);
         if (c.range != null) {
           e.setRange(new CommentRange(
@@ -393,11 +396,11 @@
         del.addAll(drafts.values());
         break;
       case PUBLISH:
+      case PUBLISH_ALL_REVISIONS:
         for (PatchLineComment e : drafts.values()) {
           e.setStatus(PatchLineComment.Status.PUBLISHED);
           e.setWrittenOn(timestamp);
-          setCommentRevId(e, patchListCache, rsrc.getChange(),
-              rsrc.getPatchSet());
+          setCommentRevId(e, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
           ups.add(e);
         }
         break;
@@ -408,8 +411,18 @@
     return !del.isEmpty() || !ups.isEmpty();
   }
 
-  private Map<String, PatchLineComment> scanDraftComments(
-      RevisionResource rsrc) throws OrmException {
+  private Map<String, PatchLineComment> changeDrafts(RevisionResource rsrc)
+      throws OrmException {
+    Map<String, PatchLineComment> drafts = Maps.newHashMap();
+    for (PatchLineComment c : plcUtil.draftByChangeAuthor(
+        db.get(), rsrc.getNotes(), rsrc.getAccountId())) {
+      drafts.put(c.getKey().get(), c);
+    }
+    return drafts;
+  }
+
+  private Map<String, PatchLineComment> patchSetDrafts(RevisionResource rsrc)
+      throws OrmException {
     Map<String, PatchLineComment> drafts = Maps.newHashMap();
     for (PatchLineComment c : plcUtil.draftByPatchSetAuthor(db.get(),
         rsrc.getPatchSet().getId(), rsrc.getAccountId(), rsrc.getNotes())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 2a0bcb3..a4a5e16 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -45,7 +45,7 @@
   private final DeleteDraftComment delete;
   private final PatchLineCommentsUtil plcUtil;
   private final ChangeUpdate.Factory updateFactory;
-  private final CommentJson commentJson;
+  private final Provider<CommentJson> commentJson;
   private final PatchListCache patchListCache;
 
   @Inject
@@ -53,7 +53,7 @@
       DeleteDraftComment delete,
       PatchLineCommentsUtil plcUtil,
       ChangeUpdate.Factory updateFactory,
-      CommentJson commentJson,
+      Provider<CommentJson> commentJson,
       PatchListCache patchListCache) {
     this.db = db;
     this.delete = delete;
@@ -96,14 +96,13 @@
           Collections.singleton(update(c, in)));
     } else {
       if (c.getRevId() == null) {
-        setCommentRevId(c, patchListCache, rsrc.getChange(),
-            rsrc.getPatchSet());
+        setCommentRevId(c, patchListCache, rsrc.getChange(), rsrc.getPatchSet());
       }
       plcUtil.updateComments(db.get(), update,
           Collections.singleton(update(c, in)));
     }
     update.commit();
-    return Response.ok(commentJson.format(c, false));
+    return Response.ok(commentJson.get().setFillAccounts(false).format(c));
   }
 
   private PatchLineComment update(PatchLineComment e, DraftInput in) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index 1e9bf2a..5aacef7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.primitives.Ints;
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
@@ -26,9 +27,10 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -37,26 +39,32 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.ArrayList;
 
 @Singleton
 public class Rebase implements RestModifyView<RevisionResource, RebaseInput>,
     UiAction<RevisionResource> {
 
-  private static final Logger log =
-      LoggerFactory.getLogger(Rebase.class);
+  private static final Logger log = LoggerFactory.getLogger(Rebase.class);
 
+  private final GitRepositoryManager repoManager;
   private final Provider<RebaseChange> rebaseChange;
   private final ChangeJson json;
   private final Provider<ReviewDb> dbProvider;
 
   @Inject
-  public Rebase(Provider<RebaseChange> rebaseChange, ChangeJson json,
+  public Rebase(GitRepositoryManager repoManager,
+      Provider<RebaseChange> rebaseChange,
+      ChangeJson json,
       Provider<ReviewDb> dbProvider) {
+    this.repoManager = repoManager;
     this.rebaseChange = rebaseChange;
     this.json = json
         .addOption(ListChangesOption.CURRENT_REVISION)
@@ -67,68 +75,24 @@
   @Override
   public ChangeInfo apply(RevisionResource rsrc, RebaseInput input)
       throws AuthException, ResourceNotFoundException,
-      ResourceConflictException, EmailException, OrmException {
+      ResourceConflictException, EmailException, OrmException, IOException {
     ChangeControl control = rsrc.getControl();
     Change change = rsrc.getChange();
-    if (!control.canRebase()) {
-      throw new AuthException("rebase not permitted");
-    } else if (!change.getStatus().isOpen()) {
-      throw new ResourceConflictException("change is "
-          + change.getStatus().name().toLowerCase());
-    } else if (!hasOneParent(rsrc.getPatchSet().getId())) {
-      throw new ResourceConflictException(
-          "cannot rebase merge commits or commit with no ancestor");
-    }
-
-    String baseRev = null;
-    if (input != null && input.base != null) {
-      String base = input.base.trim();
-      do {
-        if (base.equals("")) {
-          // remove existing dependency to other patch set
-          baseRev = change.getDest().get();
-          break;
-        }
-
-        ReviewDb db = dbProvider.get();
-        PatchSet basePatchSet = parseBase(base);
-        if (basePatchSet == null) {
-          throw new ResourceConflictException("base revision is missing: " + base);
-        } else if (!rsrc.getControl().isPatchVisible(basePatchSet, db)) {
-          throw new AuthException("base revision not accessible: " + base);
-        } else if (change.getId().equals(basePatchSet.getId().getParentKey())) {
-          throw new ResourceConflictException("cannot depend on self");
-        }
-
-        Change baseChange = db.changes().get(basePatchSet.getId().getParentKey());
-        if (baseChange != null) {
-          if (!baseChange.getProject().equals(change.getProject())) {
-            throw new ResourceConflictException("base change is in wrong project: "
-                                                + baseChange.getProject());
-          } else if (!baseChange.getDest().equals(change.getDest())) {
-            throw new ResourceConflictException("base change is targetting wrong branch: "
-                                                + baseChange.getDest());
-          } else if (baseChange.getStatus() == Status.ABANDONED) {
-            throw new ResourceConflictException("base change is abandoned: "
-                                                + baseChange.getKey());
-          } else if (isDescendantOf(baseChange.getId(), rsrc.getPatchSet().getRevision())) {
-            throw new ResourceConflictException("base change " + baseChange.getKey()
-                                                + " is a descendant of the current "
-                                                + " change - recursion not allowed");
-          }
-          baseRev = basePatchSet.getRevision().get();
-          break;
-        }
-      } while (false);  // just wanted to use the break statement
-    }
-
-    try {
-      rebaseChange.get().rebase(change, rsrc.getPatchSet().getId(),
-          rsrc.getUser(), baseRev);
+    try (Repository repo = repoManager.openRepository(change.getProject());
+        RevWalk rw = new RevWalk(repo)) {
+      if (!control.canRebase()) {
+        throw new AuthException("rebase not permitted");
+      } else if (!change.getStatus().isOpen()) {
+        throw new ResourceConflictException("change is "
+            + change.getStatus().name().toLowerCase());
+      } else if (!hasOneParent(rw, rsrc.getPatchSet())) {
+        throw new ResourceConflictException(
+            "cannot rebase merge commits or commit with no ancestor");
+      }
+      rebaseChange.get().rebase(repo, rw, change, rsrc.getPatchSet().getId(),
+          rsrc.getUser(), findBaseRev(rw, rsrc, input));
     } catch (InvalidChangeOperationException e) {
       throw new ResourceConflictException(e.getMessage());
-    } catch (IOException e) {
-      throw new ResourceConflictException(e.getMessage());
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(change.getId().toString());
     }
@@ -136,89 +100,118 @@
     return json.format(change.getId());
   }
 
-  private boolean isDescendantOf(Change.Id child, RevId ancestor)
-      throws OrmException {
-    ReviewDb db = dbProvider.get();
-
-    ArrayList<RevId> parents = new ArrayList<>();
-    parents.add(ancestor);
-    while (!parents.isEmpty()) {
-      RevId parent = parents.remove(0);
-      // get direct descendants of change
-      for (PatchSetAncestor desc : db.patchSetAncestors().descendantsOf(parent)) {
-        PatchSet descPatchSet = db.patchSets().get(desc.getPatchSet());
-        Change.Id descChangeId = descPatchSet.getId().getParentKey();
-        if (child.equals(descChangeId)) {
-          PatchSet.Id descCurrentPatchSetId =
-              db.changes().get(descChangeId).currentPatchSetId();
-          // it's only bad if the descendant patch set is current
-          return descPatchSet.getId().equals(descCurrentPatchSetId);
-        } else {
-          // process indirect descendants as well
-          parents.add(descPatchSet.getRevision());
-        }
-      }
+  private String findBaseRev(RevWalk rw, RevisionResource rsrc,
+      RebaseInput input) throws AuthException, ResourceConflictException,
+      OrmException, IOException {
+    if (input == null || input.base == null) {
+      return null;
     }
 
-    return false;
+    Change change = rsrc.getChange();
+    String base = input.base.trim();
+    if (base.equals("")) {
+      // remove existing dependency to other patch set
+      return change.getDest().get();
+    }
+
+    ReviewDb db = dbProvider.get();
+    PatchSet basePatchSet = parseBase(base);
+    if (basePatchSet == null) {
+      throw new ResourceConflictException("base revision is missing: " + base);
+    } else if (!rsrc.getControl().isPatchVisible(basePatchSet, db)) {
+      throw new AuthException("base revision not accessible: " + base);
+    } else if (change.getId().equals(basePatchSet.getId().getParentKey())) {
+      throw new ResourceConflictException("cannot depend on self");
+    }
+
+    Change baseChange = db.changes().get(basePatchSet.getId().getParentKey());
+    if (baseChange == null) {
+      return null;
+    }
+    if (!baseChange.getProject().equals(change.getProject())) {
+      throw new ResourceConflictException(
+          "base change is in wrong project: " + baseChange.getProject());
+    } else if (!baseChange.getDest().equals(change.getDest())) {
+      throw new ResourceConflictException(
+          "base change is targeting wrong branch: " + baseChange.getDest());
+    } else if (baseChange.getStatus() == Status.ABANDONED) {
+      throw new ResourceConflictException(
+          "base change is abandoned: " + baseChange.getKey());
+    } else if (isMergedInto(rw, rsrc.getPatchSet(), basePatchSet)) {
+      throw new ResourceConflictException(
+          "base change " + baseChange.getKey()
+          + " is a descendant of the current  change - recursion not allowed");
+    }
+    return basePatchSet.getRevision().get();
   }
 
-  private PatchSet parseBase(final String base) throws OrmException {
+  private boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip)
+      throws IOException {
+    ObjectId baseId = ObjectId.fromString(base.getRevision().get());
+    ObjectId tipId = ObjectId.fromString(tip.getRevision().get());
+    return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
+  }
+
+  private PatchSet parseBase(String base) throws OrmException {
     ReviewDb db = dbProvider.get();
 
     PatchSet.Id basePatchSetId = PatchSet.Id.fromRef(base);
     if (basePatchSetId != null) {
-      // try parsing the base as a ref string
+      // Try parsing the base as a ref string.
       return db.patchSets().get(basePatchSetId);
     }
 
-    // try parsing base as a change number (assume current patch set)
+    // Try parsing base as a change number (assume current patch set).
     PatchSet basePatchSet = null;
-    try {
-      Change.Id baseChangeId = Change.Id.parse(base);
-      if (baseChangeId != null) {
-        for (PatchSet ps : db.patchSets().byChange(baseChangeId)) {
-          if (basePatchSet == null || basePatchSet.getId().get() < ps.getId().get()){
-            basePatchSet = ps;
-          }
-        }
-      }
-    } catch (NumberFormatException e) {  // probably a SHA1
-    }
-
-    // try parsing as SHA1
-    if (basePatchSet == null) {
-      for (PatchSet ps : db.patchSets().byRevision(new RevId(base))) {
-        if (basePatchSet == null || basePatchSet.getId().get() < ps.getId().get()) {
+    Integer baseChangeId = Ints.tryParse(base);
+    if (baseChangeId != null) {
+      for (PatchSet ps : db.patchSets().byChange(new Change.Id(baseChangeId))) {
+        if (basePatchSet == null
+            || basePatchSet.getId().get() < ps.getId().get()) {
           basePatchSet = ps;
         }
       }
+      if (basePatchSet != null) {
+        return basePatchSet;
+      }
     }
 
+    // Try parsing as SHA-1.
+    for (PatchSet ps : db.patchSets().byRevision(new RevId(base))) {
+      if (basePatchSet == null
+          || basePatchSet.getId().get() < ps.getId().get()) {
+        basePatchSet = ps;
+      }
+    }
     return basePatchSet;
   }
 
-  private boolean hasOneParent(final PatchSet.Id patchSetId) {
-    try {
-      // prevent rebase of exotic changes (merge commit, no ancestor).
-      return (dbProvider.get().patchSetAncestors()
-          .ancestorsOf(patchSetId).toList().size() == 1);
-    } catch (OrmException e) {
-      log.error("Failed to get ancestors of patch set "
-          + patchSetId.toRefName(), e);
-      return false;
-    }
+  private boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
+    // Prevent rebase of exotic changes (merge commit, no ancestor).
+    RevCommit c = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+    return c.getParentCount() == 1;
   }
 
   @Override
   public UiAction.Description getDescription(RevisionResource resource) {
+    Project.NameKey project = resource.getChange().getProject();
+    boolean visible = resource.getChange().getStatus().isOpen()
+          && resource.isCurrent()
+          && resource.getControl().canRebase();
+    if (visible) {
+      try (Repository repo = repoManager.openRepository(project);
+          RevWalk rw = new RevWalk(repo)) {
+        visible = hasOneParent(rw, resource.getPatchSet());
+      } catch (IOException e) {
+        log.error("Failed to get ancestors of patch set "
+            + resource.getPatchSet().getId(), e);
+        visible = false;
+      }
+    }
     UiAction.Description descr = new UiAction.Description()
       .setLabel("Rebase")
       .setTitle("Rebase onto tip of branch or parent change")
-      .setVisible(resource.getChange().getStatus().isOpen()
-          && resource.isCurrent()
-          && resource.getControl().canRebase()
-          && hasOneParent(resource.getPatchSet().getId()));
+      .setVisible(visible);
     if (descr.isVisible()) {
       // Disable the rebase button in the RebaseDialog if
       // the change cannot be rebased.
@@ -239,7 +232,7 @@
     @Override
     public ChangeInfo apply(ChangeResource rsrc, RebaseInput input)
         throws AuthException, ResourceNotFoundException,
-        ResourceConflictException, EmailException, OrmException {
+        ResourceConflictException, EmailException, OrmException, IOException {
       PatchSet ps =
           rebase.dbProvider.get().patchSets()
               .get(rsrc.getChange().currentPatchSetId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java
index 5286a4a..96b513e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChange.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -50,13 +49,16 @@
 import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.List;
 import java.util.TimeZone;
 
 @Singleton
 public class RebaseChange {
+  private static final Logger log = LoggerFactory.getLogger(RebaseChange.class);
+
   private final ChangeControl.GenericFactory changeControlFactory;
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager gitManager;
@@ -65,12 +67,12 @@
   private final PatchSetInserter.Factory patchSetInserterFactory;
 
   @Inject
-  RebaseChange(final ChangeControl.GenericFactory changeControlFactory,
-      final Provider<ReviewDb> db,
-      @GerritPersonIdent final PersonIdent myIdent,
-      final GitRepositoryManager gitManager,
-      final MergeUtil.Factory mergeUtilFactory,
-      final PatchSetInserter.Factory patchSetInserterFactory) {
+  RebaseChange(ChangeControl.GenericFactory changeControlFactory,
+      Provider<ReviewDb> db,
+      @GerritPersonIdent PersonIdent myIdent,
+      GitRepositoryManager gitManager,
+      MergeUtil.Factory mergeUtilFactory,
+      PatchSetInserter.Factory patchSetInserterFactory) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.gitManager = gitManager;
@@ -80,64 +82,64 @@
   }
 
   /**
-   * Rebases the change of the given patch set.
-   *
+   * Rebase the change of the given patch set.
+   * <p>
    * It is verified that the current user is allowed to do the rebase.
-   *
+   * <p>
    * If the patch set has no dependency to an open change, then the change is
    * rebased on the tip of the destination branch.
-   *
+   * <p>
    * If the patch set depends on an open change, it is rebased on the latest
    * patch set of this change.
-   *
+   * <p>
    * The rebased commit is added as new patch set to the change.
-   *
+   * <p>
    * E-mail notification and triggering of hooks happens for the creation of the
    * new patch set.
    *
-   * @param change the change to perform the rebase for
-   * @param patchSetId the id of the patch set
-   * @param uploader the user that creates the rebased patch set
-   * @param newBaseRev the commit that should be the new base
-   * @throws NoSuchChangeException thrown if the change to which the patch set
-   *         belongs does not exist or is not visible to the user
-   * @throws EmailException thrown if sending the e-mail to notify about the new
-   *         patch set fails
-   * @throws OrmException thrown in case accessing the database fails
-   * @throws IOException thrown if rebase is not possible or not needed
-   * @throws InvalidChangeOperationException thrown if rebase is not allowed
+   * @param git the repository.
+   * @param rw the RevWalk.
+   * @param change the change to rebase.
+   * @param patchSetId the patch set ID to rebase.
+   * @param uploader the user that creates the rebased patch set.
+   * @param newBaseRev the commit that should be the new base.
+   * @throws NoSuchChangeException if the change to which the patch set belongs
+   *     does not exist or is not visible to the user.
+   * @throws EmailException if sending the e-mail to notify about the new patch
+   *     set fails.
+   * @throws OrmException if accessing the database fails.
+   * @throws IOException if accessing the repository fails.
+   * @throws InvalidChangeOperationException if rebase is not possible or not
+   *     allowed.
    */
-  public void rebase(Change change, PatchSet.Id patchSetId, final IdentifiedUser uploader,
-      final String newBaseRev) throws NoSuchChangeException, EmailException, OrmException,
-      IOException, InvalidChangeOperationException {
-    final Change.Id changeId = patchSetId.getParentKey();
-    final ChangeControl changeControl =
+  public void rebase(Repository git, RevWalk rw, Change change,
+      PatchSet.Id patchSetId, IdentifiedUser uploader, String newBaseRev)
+      throws NoSuchChangeException, EmailException, OrmException, IOException,
+      InvalidChangeOperationException {
+    Change.Id changeId = patchSetId.getParentKey();
+    ChangeControl changeControl =
         changeControlFactory.validateFor(change, uploader);
     if (!changeControl.canRebase()) {
-      throw new InvalidChangeOperationException(
-          "Cannot rebase: New patch sets are not allowed to be added to change: "
-              + changeId.toString());
+      throw new InvalidChangeOperationException("Cannot rebase: New patch sets"
+          + " are not allowed to be added to change: " + changeId);
     }
-    try (Repository git = gitManager.openRepository(change.getProject());
-        RevWalk rw = new RevWalk(git);
-        ObjectInserter inserter = git.newObjectInserter()) {
+    try (ObjectInserter inserter = git.newObjectInserter()) {
       String baseRev = newBaseRev;
       if (baseRev == null) {
-          baseRev = findBaseRevision(patchSetId, db.get(),
-              change.getDest(), git, null, null, null);
+        baseRev = findBaseRevision(
+            patchSetId, db.get(), change.getDest(), git, rw);
       }
       ObjectId baseObjectId = git.resolve(baseRev);
       if (baseObjectId == null) {
         throw new InvalidChangeOperationException(
           "Cannot rebase: Failed to resolve baseRev: " + baseRev);
       }
-      final RevCommit baseCommit = rw.parseCommit(baseObjectId);
+      RevCommit baseCommit = rw.parseCommit(baseObjectId);
 
       PersonIdent committerIdent =
-          uploader.newCommitterIdent(TimeUtil.nowTs(),
-              serverTimeZone);
+          uploader.newCommitterIdent(TimeUtil.nowTs(), serverTimeZone);
 
-      rebase(git, rw, inserter, patchSetId, change,
+      rebase(git, rw, inserter, change, patchSetId,
           uploader, baseCommit, mergeUtilFactory.create(
               changeControl.getProjectControl().getProjectState(), true),
           committerIdent, true, ValidatePolicy.GERRIT);
@@ -147,158 +149,150 @@
   }
 
   /**
-   * Finds the revision of commit on which the given patch set should be based.
+   * Find the commit onto which a patch set should be rebased.
+   * <p>
+   * This is defined as the latest patch set of the change corresponding to
+   * this commit's parent, or the destination branch tip in the case where the
+   * parent's change is merged.
    *
-   * @param patchSetId the id of the patch set for which the new base commit
-   *        should be found
-   * @param db the ReviewDb
-   * @param destBranch the destination branch
-   * @param git the repository
-   * @param patchSetAncestors the original PatchSetAncestor of the given patch
-   *        set that should be based
-   * @param depPatchSetList the original patch set list on which the rebased
-   *        patch set depends
-   * @param depChangeList the original change list on whose patch set the
-   *        rebased patch set depends
-   * @return the revision of commit on which the given patch set should be based
-   * @throws IOException thrown if rebase is not possible or not needed
-   * @throws OrmException thrown in case accessing the database fails
+   * @param patchSetId patch set ID for which the new base commit should be
+   *     found.
+   * @param db the ReviewDb.
+   * @param destBranch the destination branch.
+   * @param git the repository.
+   * @param rw the RevWalk.
+   * @return the commit onto which the patch set should be rebased.
+   * @throws InvalidChangeOperationException if rebase is not possible or not
+   *     allowed.
+   * @throws IOException if accessing the repository fails.
+   * @throws OrmException if accessing the database fails.
    */
-    private static String findBaseRevision(final PatchSet.Id patchSetId,
-        final ReviewDb db, final Branch.NameKey destBranch, final Repository git,
-        List<PatchSetAncestor> patchSetAncestors, List<PatchSet> depPatchSetList,
-        List<Change> depChangeList) throws IOException, OrmException {
+  private static String findBaseRevision(PatchSet.Id patchSetId,
+      ReviewDb db, Branch.NameKey destBranch, Repository git, RevWalk rw)
+      throws InvalidChangeOperationException, IOException, OrmException {
+    String baseRev = null;
 
-      String baseRev = null;
+    PatchSet patchSet = db.patchSets().get(patchSetId);
+    if (patchSet == null) {
+      throw new InvalidChangeOperationException(
+          "Patch set " + patchSetId + " not found");
+    }
+    RevCommit commit = rw.parseCommit(
+        ObjectId.fromString(patchSet.getRevision().get()));
 
-      if (patchSetAncestors == null) {
-        patchSetAncestors =
-            db.patchSetAncestors().ancestorsOf(patchSetId).toList();
-      }
-
-      if (patchSetAncestors.size() > 1) {
-        throw new IOException(
-            "Cannot rebase a change with multiple parents. Parent commits: "
-                + patchSetAncestors.toString());
-      }
-      if (patchSetAncestors.size() == 0) {
-        throw new IOException(
-            "Cannot rebase a change without any parents (is this the initial commit?).");
-      }
-
-      RevId ancestorRev = patchSetAncestors.get(0).getAncestorRevision();
-      if (depPatchSetList == null || depPatchSetList.size() != 1 ||
-          !depPatchSetList.get(0).getRevision().equals(ancestorRev)) {
-        depPatchSetList = db.patchSets().byRevision(ancestorRev).toList();
-      }
-
-      for (PatchSet depPatchSet : depPatchSetList) {
-
-        Change.Id depChangeId = depPatchSet.getId().getParentKey();
-        Change depChange;
-        if (depChangeList == null || depChangeList.size() != 1 ||
-            !depChangeList.get(0).getId().equals(depChangeId)) {
-          depChange = db.changes().get(depChangeId);
-        } else {
-          depChange = depChangeList.get(0);
-        }
-        if (!depChange.getDest().equals(destBranch)) {
-          continue;
-        }
-
-        if (depChange.getStatus() == Status.ABANDONED) {
-          throw new IOException("Cannot rebase a change with an abandoned parent: "
-              + depChange.getKey().toString());
-        }
-
-        if (depChange.getStatus().isOpen()) {
-          if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
-            throw new IOException(
-                "Change is already based on the latest patch set of the dependent change.");
-          }
-          PatchSet latestDepPatchSet =
-              db.patchSets().get(depChange.currentPatchSetId());
-          baseRev = latestDepPatchSet.getRevision().get();
-        }
-        break;
-      }
-
-      if (baseRev == null) {
-        // We are dependent on a merged PatchSet or have no PatchSet
-        // dependencies at all.
-        Ref destRef = git.getRef(destBranch.get());
-        if (destRef == null) {
-          throw new IOException(
-              "The destination branch does not exist: "
-                  + destBranch.get());
-        }
-        baseRev = destRef.getObjectId().getName();
-        if (baseRev.equals(ancestorRev.get())) {
-          throw new IOException("Change is already up to date.");
-        }
-      }
-      return baseRev;
+    if (commit.getParentCount() > 1) {
+      throw new InvalidChangeOperationException(
+          "Cannot rebase a change with multiple parents.");
+    } else if (commit.getParentCount() == 0) {
+      throw new InvalidChangeOperationException(
+          "Cannot rebase a change without any parents"
+          + " (is this the initial commit?).");
     }
 
+    RevId parentRev = new RevId(commit.getParent(0).name());
+
+    for (PatchSet depPatchSet : db.patchSets().byRevision(parentRev)) {
+      Change.Id depChangeId = depPatchSet.getId().getParentKey();
+      Change depChange = db.changes().get(depChangeId);
+      if (!depChange.getDest().equals(destBranch)) {
+        continue;
+      }
+
+      if (depChange.getStatus() == Status.ABANDONED) {
+        throw new InvalidChangeOperationException(
+            "Cannot rebase a change with an abandoned parent: "
+            + depChange.getKey());
+      }
+
+      if (depChange.getStatus().isOpen()) {
+        if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
+          throw new InvalidChangeOperationException(
+              "Change is already based on the latest patch set of the"
+              + " dependent change.");
+        }
+        PatchSet latestDepPatchSet =
+            db.patchSets().get(depChange.currentPatchSetId());
+        baseRev = latestDepPatchSet.getRevision().get();
+      }
+      break;
+    }
+
+    if (baseRev == null) {
+      // We are dependent on a merged PatchSet or have no PatchSet
+      // dependencies at all.
+      Ref destRef = git.getRef(destBranch.get());
+      if (destRef == null) {
+        throw new InvalidChangeOperationException(
+            "The destination branch does not exist: " + destBranch.get());
+      }
+      baseRev = destRef.getObjectId().getName();
+      if (baseRev.equals(parentRev.get())) {
+        throw new InvalidChangeOperationException(
+            "Change is already up to date.");
+      }
+    }
+    return baseRev;
+  }
+
   /**
-   * Rebases the change of the given patch set on the given base commit.
-   *
+   * Rebase the change of the given patch set on the given base commit.
+   * <p>
    * The rebased commit is added as new patch set to the change.
+   * <p>
+   * E-mail notification and triggering of hooks is only done for the creation
+   * of the new patch set if {@code sendEmail} and {@code runHooks} are true,
+   * respectively.
    *
-   * E-mail notification and triggering of hooks is only done for the creation of
-   * the new patch set if `sendEmail` and `runHooks` are set to true.
-   *
-   * @param git the repository
-   * @param revWalk the RevWalk
-   * @param inserter the object inserter
-   * @param patchSetId the id of the patch set
-   * @param change the change that should be rebased
-   * @param uploader the user that creates the rebased patch set
-   * @param baseCommit the commit that should be the new base
-   * @param mergeUtil merge utilities for the destination project
-   * @param committerIdent the committer's identity
-   * @param runHooks if hooks should be run for the new patch set
-   * @param validate if commit validation should be run for the new patch set
-   * @return the new patch set which is based on the given base commit
-   * @throws NoSuchChangeException thrown if the change to which the patch set
-   *         belongs does not exist or is not visible to the user
-   * @throws OrmException thrown in case accessing the database fails
-   * @throws IOException thrown if rebase is not possible or not needed
-   * @throws InvalidChangeOperationException thrown if rebase is not allowed
+   * @param git the repository.
+   * @param inserter the object inserter.
+   * @param change the change to rebase.
+   * @param patchSetId the patch set ID to rebase.
+   * @param uploader the user that creates the rebased patch set.
+   * @param baseCommit the commit that should be the new base.
+   * @param mergeUtil merge utilities for the destination project.
+   * @param committerIdent the committer's identity.
+   * @param runHooks if hooks should be run for the new patch set.
+   * @param validate if commit validation should be run for the new patch set.
+   * @param rw the RevWalk.
+   * @return the new patch set, which is based on the given base commit.
+   * @throws NoSuchChangeException if the change to which the patch set belongs
+   *     does not exist or is not visible to the user.
+   * @throws OrmException if accessing the database fails.
+   * @throws IOException if rebase is not possible.
+   * @throws InvalidChangeOperationException if rebase is not possible or not
+   *     allowed.
    */
-  public PatchSet rebase(final Repository git, final RevWalk revWalk,
-      final ObjectInserter inserter, final PatchSet.Id patchSetId,
-      final Change change, final IdentifiedUser uploader, final RevCommit baseCommit,
-      final MergeUtil mergeUtil, PersonIdent committerIdent,
-      boolean runHooks, ValidatePolicy validate)
-          throws NoSuchChangeException,
-      OrmException, IOException, InvalidChangeOperationException,
-      MergeConflictException {
+  public PatchSet rebase(Repository git, RevWalk rw,
+      ObjectInserter inserter, Change change, PatchSet.Id patchSetId,
+      IdentifiedUser uploader, RevCommit baseCommit, MergeUtil mergeUtil,
+      PersonIdent committerIdent, boolean runHooks, ValidatePolicy validate)
+      throws NoSuchChangeException, OrmException, IOException,
+      InvalidChangeOperationException, MergeConflictException {
     if (!change.currentPatchSetId().equals(patchSetId)) {
       throw new InvalidChangeOperationException("patch set is not current");
     }
-    final PatchSet originalPatchSet = db.get().patchSets().get(patchSetId);
+    PatchSet originalPatchSet = db.get().patchSets().get(patchSetId);
 
-    final RevCommit rebasedCommit;
+    RevCommit rebasedCommit;
     ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
-    ObjectId newId = rebaseCommit(git, inserter, revWalk.parseCommit(oldId),
+    ObjectId newId = rebaseCommit(git, inserter, rw.parseCommit(oldId),
         baseCommit, mergeUtil, committerIdent);
 
-    rebasedCommit = revWalk.parseCommit(newId);
+    rebasedCommit = rw.parseCommit(newId);
 
-    final ChangeControl changeControl =
+    ChangeControl changeControl =
         changeControlFactory.validateFor(change, uploader);
 
     PatchSetInserter patchSetInserter = patchSetInserterFactory
-        .create(git, revWalk, changeControl, rebasedCommit)
+        .create(git, rw, changeControl, rebasedCommit)
         .setValidatePolicy(validate)
         .setDraft(originalPatchSet.isDraft())
         .setUploader(uploader.getAccountId())
         .setSendMail(false)
         .setRunHooks(runHooks);
 
-    final PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
-    final ChangeMessage cmsg = new ChangeMessage(
+    PatchSet.Id newPatchSetId = patchSetInserter.getPatchSetId();
+    ChangeMessage cmsg = new ChangeMessage(
         new ChangeMessage.Key(change.getId(),
             ChangeUtil.messageUUID(db.get())), uploader.getAccountId(),
             TimeUtil.nowTs(), patchSetId);
@@ -328,11 +322,13 @@
    */
   private ObjectId rebaseCommit(Repository git, ObjectInserter inserter,
       RevCommit original, RevCommit base, MergeUtil mergeUtil,
-      PersonIdent committerIdent) throws MergeConflictException, IOException {
+      PersonIdent committerIdent) throws MergeConflictException, IOException,
+      InvalidChangeOperationException {
     RevCommit parentCommit = original.getParent(0);
 
     if (base.equals(parentCommit)) {
-      throw new IOException("Change is already up to date.");
+      throw new InvalidChangeOperationException(
+          "Change is already up to date.");
     }
 
     ThreeWayMerger merger = mergeUtil.newThreeWayMerger(git, inserter);
@@ -365,8 +361,8 @@
         r.getPatchSet().getId(), r.getChange().getDest());
   }
 
-  public boolean canRebase(Project.NameKey project,
-      PatchSet.Id patchSetId, Branch.NameKey branch) {
+  public boolean canRebase(Project.NameKey project, PatchSet.Id patchSetId,
+      Branch.NameKey branch) {
     Repository git;
     try {
       git = gitManager.openRepository(project);
@@ -375,19 +371,14 @@
     } catch (IOException err) {
       return false;
     }
-    try {
-      findBaseRevision(
-          patchSetId,
-          db.get(),
-          branch,
-          git,
-          null,
-          null,
-          null);
+    try (RevWalk rw = new RevWalk(git)) {
+      findBaseRevision(patchSetId, db.get(), branch, git, rw);
       return true;
-    } catch (IOException e) {
+    } catch (InvalidChangeOperationException e) {
       return false;
-    } catch (OrmException e) {
+    } catch (OrmException | IOException e) {
+      log.warn("Error checking if patch set " + patchSetId + " on " + branch
+          + " can be rebased", e);
       return false;
     } finally {
       git.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index 65b390a..7c10b11 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -113,7 +113,7 @@
           .setPatchSet(ps)
           .setFastEvalLabels(true)
           .setAllowDraft(true)
-          .canSubmit()) {
+          .evaluate()) {
         if (rec.labels == null) {
           continue;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
index 4120e43..9e243b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestionCache.java
@@ -14,22 +14,48 @@
 
 package com.google.gerrit.server.change;
 
-import com.google.common.base.Predicate;
+import com.google.common.base.Splitter;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.AccountExternalIdAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.analysis.util.CharArraySet;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.document.IntField;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.PrefixQuery;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.RAMDirectory;
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.Collections;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
@@ -44,34 +70,115 @@
 public class ReviewerSuggestionCache {
   private static final Logger log = LoggerFactory
       .getLogger(ReviewerSuggestionCache.class);
-  private final LoadingCache<Boolean, List<Account>> cache;
+
+  private static final String ID = "id";
+  private static final String NAME = "name";
+  private static final String EMAIL = "email";
+  private static final String USERNAME = "username";
+  private static final String[] ALL = {ID, NAME, EMAIL, USERNAME};
+
+  private final LoadingCache<Boolean, IndexSearcher> cache;
+  private final Provider<ReviewDb> db;
 
   @Inject
-  ReviewerSuggestionCache(final Provider<ReviewDb> dbProvider) {
+  ReviewerSuggestionCache(Provider<ReviewDb> db,
+      @GerritServerConfig Config cfg) {
+    this.db = db;
+    long expiration = ConfigUtil.getTimeUnit(cfg,
+        "suggest", null, "fullTextSearchRefresh",
+        TimeUnit.HOURS.toMillis(1),
+        TimeUnit.MILLISECONDS);
     this.cache =
         CacheBuilder.newBuilder().maximumSize(1)
-            .expireAfterWrite(30, TimeUnit.SECONDS)
-            .build(new CacheLoader<Boolean, List<Account>>() {
+            .expireAfterWrite(expiration, TimeUnit.MILLISECONDS)
+            .build(new CacheLoader<Boolean, IndexSearcher>() {
               @Override
-              public List<Account> load(Boolean key) throws Exception {
-                return ImmutableList.copyOf(Iterables.filter(
-                    dbProvider.get().accounts().all(),
-                    new Predicate<Account>() {
-                      @Override
-                      public boolean apply(Account in) {
-                        return in.isActive();
-                      }
-                    }));
+              public IndexSearcher load(Boolean key) throws Exception {
+                return index();
               }
             });
   }
 
-  List<Account> get() {
+  List<AccountInfo> search(String query, int n) throws IOException {
+    IndexSearcher searcher = get();
+    if (searcher == null) {
+      return Collections.emptyList();
+    }
+
+    List<String> segments = Splitter.on(' ').omitEmptyStrings().splitToList(
+        query.toLowerCase());
+    BooleanQuery q = new BooleanQuery();
+    for (String field : ALL) {
+      BooleanQuery and = new BooleanQuery();
+      for (String s : segments) {
+        and.add(new PrefixQuery(new Term(field, s)), Occur.MUST);
+      }
+      q.add(and, Occur.SHOULD);
+    }
+
+    TopDocs results = searcher.search(q, n);
+    ScoreDoc[] hits = results.scoreDocs;
+
+    List<AccountInfo> result = new LinkedList<>();
+
+    for (ScoreDoc h : hits) {
+      Document doc = searcher.doc(h.doc);
+
+      AccountInfo info = new AccountInfo(
+          doc.getField(ID).numericValue().intValue());
+      info.name = doc.get(NAME);
+      info.email = doc.get(EMAIL);
+      info.username = doc.get(USERNAME);
+      result.add(info);
+    }
+
+    return result;
+  }
+
+  private IndexSearcher get() {
     try {
       return cache.get(true);
     } catch (ExecutionException e) {
       log.warn("Cannot fetch reviewers from cache", e);
-      return Collections.emptyList();
+      return null;
     }
   }
+
+  private IndexSearcher index() throws IOException, OrmException {
+    RAMDirectory idx = new RAMDirectory();
+    IndexWriterConfig config = new IndexWriterConfig(
+        new StandardAnalyzer(CharArraySet.EMPTY_SET));
+    config.setOpenMode(OpenMode.CREATE);
+
+    try (IndexWriter writer = new IndexWriter(idx, config)) {
+      for (Account a : db.get().accounts().all()) {
+        if (a.isActive()) {
+          addAccount(writer, a);
+        }
+      }
+    }
+
+    return new IndexSearcher(DirectoryReader.open(idx));
+  }
+
+  private void addAccount(IndexWriter writer, Account a)
+      throws IOException, OrmException {
+    Document doc = new Document();
+    doc.add(new IntField(ID, a.getId().get(), Store.YES));
+    if (a.getFullName() != null) {
+      doc.add(new TextField(NAME, a.getFullName(), Store.YES));
+    }
+    if (a.getPreferredEmail() != null) {
+      doc.add(new StringField(EMAIL, a.getPreferredEmail().toLowerCase(),
+          Store.YES));
+      doc.add(new TextField(EMAIL, a.getPreferredEmail(), Store.YES));
+    }
+    AccountExternalIdAccess extIdAccess = db.get().accountExternalIds();
+    String username = AccountState.getUserName(
+        extIdAccess.byAccount(a.getId()).toList());
+    if (username != null) {
+      doc.add(new StringField(USERNAME, username, Store.YES));
+    }
+    writer.addDocument(doc);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
index a18df5b..538f36b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -92,7 +92,7 @@
     return this;
   }
 
-  Optional<ChangeEdit> getEdit() {
+  public Optional<ChangeEdit> getEdit() {
     return edit;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index f7fab69..109d96f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -101,6 +101,8 @@
       "Other hidden changes in this topic are not ready";
   private static final String CLICK_FAILURE_OTHER_TOOLTIP =
       "Clicking the button would fail for other changes in the topic";
+  private static final String CLICK_FAILURE_TOOLTIP =
+      "Clicking the button would fail.";
 
   public enum Status {
     SUBMITTED, MERGED
@@ -265,7 +267,12 @@
         // TODO(dborowitz): This is ugly; consider providing a way to not read
         // stored fields from the index in the first place.
         c.setMergeable(null);
-        if (!c.isMergeable()) {
+        Boolean mergeable = c.isMergeable();
+        if (mergeable == null) {
+          log.error("Ephemeral error checking if change is submittable");
+          return CLICK_FAILURE_TOOLTIP;
+        }
+        if (!mergeable) {
           return CLICK_FAILURE_OTHER_TOOLTIP;
         }
         checkSubmitRule(c, c.currentPatchSet(), false);
@@ -306,7 +313,7 @@
         .setVisible(false);
     }
 
-    boolean enabled;
+    Boolean enabled;
     try {
       enabled = cd.isMergeable();
     } catch (OrmException e) {
@@ -336,7 +343,7 @@
           .setTitle(Strings.emptyToNull(
               submitTopicTooltip.replace(params)))
           .setVisible(true)
-          .setEnabled(enabled);
+          .setEnabled(Boolean.TRUE.equals(enabled));
       }
     } else {
       RevId revId = resource.getPatchSet().getRevision();
@@ -348,7 +355,7 @@
         .setLabel(label)
         .setTitle(Strings.emptyToNull(titlePattern.replace(params)))
         .setVisible(true)
-        .setEnabled(enabled);
+        .setEnabled(Boolean.TRUE.equals(enabled));
     }
   }
 
@@ -573,7 +580,7 @@
           throws ResourceConflictException, OrmException {
     List<SubmitRecord> results = new SubmitRuleEvaluator(cd)
         .setPatchSet(patchSet)
-        .canSubmit();
+        .evaluate();
     Optional<SubmitRecord> ok = findOkRecord(results);
     if (ok.isPresent()) {
       // Rules supplied a valid solution.
@@ -582,7 +589,7 @@
       return results;
     } else if (results.isEmpty()) {
       throw new IllegalStateException(String.format(
-          "ChangeControl.canSubmit returned empty list for %s in %s",
+          "SubmitRuleEvaluator.evaluate returned empty list for %s in %s",
           patchSet.getId(),
           cd.change().getProject().get()));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
index 2c82f58..4561ae4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -55,6 +55,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -279,41 +280,20 @@
   }
 
   private List<AccountInfo> suggestAccountFullTextSearch(
-      VisibilityControl visibilityControl) throws OrmException {
-    String str = query.toLowerCase();
-    Map<Account.Id, AccountInfo> accountMap = new LinkedHashMap<>();
-    List<Account> fullNameMatches = new ArrayList<>(fullTextMaxMatches);
-    List<Account> emailMatches = new ArrayList<>(fullTextMaxMatches);
+      VisibilityControl visibilityControl) throws IOException, OrmException {
+    List<AccountInfo> results = reviewerSuggestionCache.search(
+        query, fullTextMaxMatches);
 
-    for (Account a : reviewerSuggestionCache.get()) {
-      if (a.getFullName() != null
-          && a.getFullName().toLowerCase().contains(str)) {
-        fullNameMatches.add(a);
-      } else if (a.getPreferredEmail() != null
-          && emailMatches.size() < fullTextMaxMatches
-          && a.getPreferredEmail().toLowerCase().contains(str)) {
-        emailMatches.add(a);
-      }
-      if (fullNameMatches.size() >= fullTextMaxMatches) {
-        break;
+    Iterator<AccountInfo> it = results.iterator();
+    while (it.hasNext()) {
+      Account.Id accountId = new Account.Id(it.next()._accountId);
+      if (!(visibilityControl.isVisibleTo(accountId)
+          && accountControl.canSee(accountId))) {
+        it.remove();
       }
     }
-    for (Account a : fullNameMatches) {
-      addSuggestion(accountMap, a.getId(), visibilityControl);
-      if (accountMap.size() >= limit) {
-        break;
-      }
-    }
-    if (accountMap.size() < limit) {
-      for (Account a : emailMatches) {
-        addSuggestion(accountMap, a.getId(), visibilityControl);
-        if (accountMap.size() >= limit) {
-          break;
-        }
-      }
-    }
-    accountLoader.fill();
-    return Lists.newArrayList(accountMap.values());
+
+    return results;
   }
 
   private boolean addSuggestion(Map<Account.Id, AccountInfo> map,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
index b037931..31e34cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -85,7 +85,7 @@
           .setLogErrors(false)
           .setSkipSubmitFilters(input.filters == Filters.SKIP)
           .setRule(input.rule)
-          .canSubmit();
+          .evaluate();
     List<Record> out = Lists.newArrayListWithCapacity(records.size());
     AccountLoader accounts = accountInfoFactory.create(true);
     for (SubmitRecord r : records) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index d7138b3..abcd441 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -44,6 +44,7 @@
   private final boolean enableRunAs;
   private final boolean userNameToLowerCase;
   private final boolean gitBasicAuth;
+  private final boolean useContributorAgreements;
   private final String loginUrl;
   private final String logoutUrl;
   private final String openIdSsoUrl;
@@ -75,6 +76,8 @@
     trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
     enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
     gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
+    useContributorAgreements =
+        cfg.getBoolean("auth", "contributoragreements", false);
     userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
 
 
@@ -194,6 +197,11 @@
     return gitBasicAuth;
   }
 
+  /** Whether contributor agreements are used. */
+  public boolean isUseContributorAgreements() {
+    return useContributorAgreements;
+  }
+
   public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) {
     switch (getAuthType()) {
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
index 2d9f21a..4986989 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -16,12 +16,14 @@
 
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.server.change.ArchiveFormat;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
 
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -31,6 +33,7 @@
 public class DownloadConfig {
   private final Set<DownloadScheme> downloadSchemes;
   private final Set<DownloadCommand> downloadCommands;
+  private final Set<ArchiveFormat> archiveFormats;
 
   @Inject
   DownloadConfig(@GerritServerConfig final Config cfg) {
@@ -45,6 +48,17 @@
             DownloadCommand.DEFAULT_DOWNLOADS);
     downloadCommands =
         Collections.unmodifiableSet(new HashSet<>(allCommands));
+
+    String v = cfg.getString("download", null, "archive");
+    if (v == null) {
+      archiveFormats = EnumSet.allOf(ArchiveFormat.class);
+    } else if (v.isEmpty() || "off".equalsIgnoreCase(v)) {
+      archiveFormats = Collections.emptySet();
+    } else {
+      archiveFormats = new HashSet<>(ConfigUtil.getEnumList(cfg,
+          "download", null, "archive",
+          ArchiveFormat.TGZ));
+    }
   }
 
   /** Scheme used to download. */
@@ -56,4 +70,9 @@
   public Set<DownloadCommand> getDownloadCommands() {
     return downloadCommands;
   }
+
+  /** Archive formats for downloading. */
+  public Set<ArchiveFormat> getArchiveFormats() {
+    return archiveFormats;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 99ff581..8e9d7e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.config.CapabilityDefinition;
 import com.google.gerrit.extensions.config.DownloadCommand;
 import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.events.GarbageCollectorListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -76,7 +77,6 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReceivePackInitializer;
-import com.google.gerrit.server.git.SearchingChangeCacheImpl;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
@@ -158,7 +158,6 @@
     install(authModule);
     install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
-    install(SearchingChangeCacheImpl.module());
     install(ChangeKindCacheImpl.module());
     install(ConflictsCacheImpl.module());
     install(GroupCacheImpl.module());
@@ -255,9 +254,9 @@
     DynamicSet.setOf(binder(), PreUploadHook.class);
     DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
     DynamicSet.setOf(binder(), ProjectDeletedListener.class);
+    DynamicSet.setOf(binder(), GarbageCollectorListener.class);
     DynamicSet.setOf(binder(), HeadUpdatedListener.class);
     DynamicSet.setOf(binder(), UsageDataPublishedListener.class);
-    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(SearchingChangeCacheImpl.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReindexAfterUpdate.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
         .to(ProjectConfigEntry.UpdateChecker.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
new file mode 100644
index 0000000..fe81dfd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -0,0 +1,176 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.config.DownloadCommand;
+import com.google.gerrit.extensions.config.DownloadScheme;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class GetServerInfo implements RestReadView<ConfigResource> {
+  private final Config config;
+  private final AuthConfig authConfig;
+  private final Realm realm;
+  private final DownloadConfig downloadConfig;
+  private final DynamicMap<DownloadScheme> downloadSchemes;
+  private final DynamicMap<DownloadCommand> downloadCommands;
+  private final AllProjectsName allProjectsName;
+  private final AllUsersName allUsersName;
+
+  @Inject
+  public GetServerInfo(
+      @GerritServerConfig Config config,
+      AuthConfig authConfig,
+      Realm realm,
+      DownloadConfig downloadConfig,
+      DynamicMap<DownloadScheme> downloadSchemes,
+      DynamicMap<DownloadCommand> downloadCommands,
+      AllProjectsName allProjectsName,
+      AllUsersName allUsersName) {
+    this.config = config;
+    this.authConfig = authConfig;
+    this.realm = realm;
+    this.downloadConfig = downloadConfig;
+    this.downloadSchemes = downloadSchemes;
+    this.downloadCommands = downloadCommands;
+    this.allProjectsName = allProjectsName;
+    this.allUsersName = allUsersName;
+  }
+
+  @Override
+  public ServerInfo apply(ConfigResource rsrc) throws MalformedURLException {
+    ServerInfo info = new ServerInfo();
+    info.auth = new AuthInfo(authConfig, realm);
+    info.contactStore = getContactStoreInfo();
+    info.download =
+        new DownloadInfo(downloadConfig, downloadSchemes, downloadCommands);
+    info.gerrit = new GerritInfo(allProjectsName, allUsersName);
+    return info;
+  }
+
+  private ContactStoreInfo getContactStoreInfo() {
+    String url = config.getString("contactstore", null, "url");
+    if (url == null) {
+      return null;
+    }
+
+    ContactStoreInfo contactStore = new ContactStoreInfo();
+    contactStore.url = url;
+    return contactStore;
+  }
+
+  private static Boolean toBoolean(boolean v) {
+    return v ? v : null;
+  }
+
+  public static class ServerInfo {
+    public AuthInfo auth;
+    public ContactStoreInfo contactStore;
+    public DownloadInfo download;
+    public GerritInfo gerrit;
+  }
+
+  public static class AuthInfo {
+    public AuthType authType;
+    public Boolean useContributorAgreements;
+    public List<Account.FieldName> editableAccountFields;
+
+    public AuthInfo(AuthConfig cfg, Realm realm) {
+      authType = cfg.getAuthType();
+      useContributorAgreements = toBoolean(cfg.isUseContributorAgreements());
+      editableAccountFields = new ArrayList<>(realm.getEditableFields());
+    }
+  }
+
+  public static class ContactStoreInfo {
+    public String url;
+  }
+
+  public static class DownloadInfo {
+    public Map<String, DownloadSchemeInfo> schemes;
+    public List<String> archives;
+
+    public DownloadInfo(DownloadConfig downloadConfig,
+        DynamicMap<DownloadScheme> downloadSchemes,
+        DynamicMap<DownloadCommand> downloadCommands) {
+      schemes = new HashMap<>();
+      for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
+        DownloadScheme scheme = e.getProvider().get();
+        if (scheme.isEnabled() && scheme.getUrl("${project}") != null) {
+          schemes.put(e.getExportName(),
+              new DownloadSchemeInfo(scheme, downloadCommands));
+        }
+      }
+      archives =
+          Lists.transform(new ArrayList<>(downloadConfig.getArchiveFormats()),
+              new Function<ArchiveFormat, String>() {
+                @Override
+                public String apply(ArchiveFormat archiveFormat) {
+                  return archiveFormat.name().toLowerCase(Locale.US);
+                }
+              });
+    }
+  }
+
+  public static class DownloadSchemeInfo {
+    public String url;
+    public Boolean isAuthRequired;
+    public Boolean isAuthSupported;
+    public Map<String, String> commands;
+
+    public DownloadSchemeInfo(DownloadScheme scheme,
+        DynamicMap<DownloadCommand> downloadCommands) {
+      url = scheme.getUrl("${project}");
+      isAuthRequired = toBoolean(scheme.isAuthRequired());
+      isAuthSupported = toBoolean(scheme.isAuthSupported());
+
+      commands = new HashMap<>();
+      for (DynamicMap.Entry<DownloadCommand> e : downloadCommands) {
+        String commandName = e.getExportName();
+        DownloadCommand command = e.getProvider().get();
+        String c = command.getCommand(scheme, "${project}", "${ref}");
+        if (c != null) {
+          commands.put(commandName, c);
+        }
+      }
+    }
+  }
+
+  public static class GerritInfo {
+    public String allProjects;
+    public String allUsers;
+
+    public GerritInfo(AllProjectsName allProjectsName, AllUsersName allUsersName) {
+      allProjects = allProjectsName.get();
+      allUsers = allUsersName.get();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
index df6d86b..006419b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListCaches.java
@@ -41,7 +41,7 @@
   private final DynamicMap<Cache<?, ?>> cacheMap;
 
   public static enum OutputFormat {
-    LIST, TEXT_LIST;
+    LIST, TEXT_LIST
   }
 
   @Option(name = "--format", usage = "output format")
@@ -85,7 +85,7 @@
   }
 
   public enum CacheType {
-    MEM, DISK;
+    MEM, DISK
   }
 
   public static class CacheInfo {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
index 64848ba..31fdc1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -35,6 +35,7 @@
     delete(TASK_KIND).to(DeleteTask.class);
     child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
     get(CONFIG_KIND, "version").to(GetVersion.class);
+    get(CONFIG_KIND, "info").to(GetServerInfo.class);
     get(CONFIG_KIND, "preferences").to(GetPreferences.class);
     put(CONFIG_KIND, "preferences").to(SetPreferences.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
index 7302ea1..2bbb731 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
@@ -52,7 +52,7 @@
   }
 
   public static enum Operation {
-    FLUSH_ALL, FLUSH;
+    FLUSH_ALL, FLUSH
   }
 
   private final DynamicMap<Cache<?, ?>> cacheMap;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index f0c0bc1..4a61e3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -151,7 +151,7 @@
     ru.newRev = newId != null ? newId.getName() : ObjectId.zeroId().getName();
     ru.oldRev = oldId != null ? oldId.getName() : ObjectId.zeroId().getName();
     ru.project = refName.getParentKey().get();
-    ru.refName = refName.getShortName();
+    ru.refName = refName.get();
     return ru;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java
index f9844d5..f044342 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BranchOrderSection.java
@@ -14,9 +14,8 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.client.RefNames;
-
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.RefNames;
 
 import java.util.List;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCacheImplModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCacheImplModule.java
new file mode 100644
index 0000000..90109a9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCacheImplModule.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.AbstractModule;
+
+public class ChangeCacheImplModule extends AbstractModule {
+  private final boolean slave;
+
+  public ChangeCacheImplModule(boolean slave) {
+    this.slave = slave;
+  }
+
+  @Override
+  protected void configure() {
+    if (slave) {
+      install(ScanningChangeCacheImpl.module());
+    } else {
+      install(SearchingChangeCacheImpl.module());
+      DynamicSet.bind(binder(), GitReferenceUpdatedListener.class)
+          .to(SearchingChangeCacheImpl.class);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
index 0c39d67..a915a79 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -16,6 +16,9 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.extensions.events.GarbageCollectorListener;
+import com.google.gerrit.extensions.events.GarbageCollectorListener.Event;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GcConfig;
 import com.google.inject.Inject;
@@ -48,6 +51,7 @@
   private final GitRepositoryManager repoManager;
   private final GarbageCollectionQueue gcQueue;
   private final GcConfig gcConfig;
+  private final DynamicSet<GarbageCollectorListener> listeners;
 
   public interface Factory {
     GarbageCollection create();
@@ -55,10 +59,12 @@
 
   @Inject
   GarbageCollection(GitRepositoryManager repoManager,
-      GarbageCollectionQueue gcQueue, GcConfig config) {
+      GarbageCollectionQueue gcQueue, GcConfig config,
+      DynamicSet<GarbageCollectorListener> listeners) {
     this.repoManager = repoManager;
     this.gcQueue = gcQueue;
     this.gcConfig = config;
+    this.listeners = listeners;
   }
 
   public GarbageCollectionResult run(List<Project.NameKey> projectNames) {
@@ -93,6 +99,7 @@
         Properties statistics = gc.call();
         logGcInfo(p, "after: ", statistics);
         print(writer, "done.\n\n");
+        fire(p, statistics);
       } catch (RepositoryNotFoundException e) {
         logGcError(writer, p, e);
         result.addError(new GarbageCollectionResult.Error(
@@ -112,6 +119,27 @@
     return result;
   }
 
+  private void fire(final Project.NameKey p, final Properties statistics) {
+    Event event = new GarbageCollectorListener.Event() {
+      @Override
+      public String getProjectName() {
+        return p.get();
+      }
+
+      @Override
+      public Properties getStatistics() {
+        return statistics;
+      }
+    };
+    for (GarbageCollectorListener l : listeners) {
+      try {
+        l.onGarbageCollected(event);
+      } catch (RuntimeException e) {
+        log.warn("Failure in GarbageCollectorListener", e);
+      }
+    }
+  }
+
   private static void logGcInfo(Project.NameKey projectName, String msg) {
     logGcInfo(projectName, msg, null);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 295ad52..da38a58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -25,13 +25,16 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
@@ -195,7 +198,9 @@
     }
   }
 
-  public String createCherryPickCommitMessage(final CodeReviewCommit n) {
+  public String createCherryPickCommitMessage(RevCommit n, ChangeControl ctl,
+      PatchSet.Id psId) {
+    Change c = ctl.getChange();
     final List<FooterLine> footers = n.getFooterLines();
     final StringBuilder msgbuf = new StringBuilder();
     msgbuf.append(n.getFullMessage());
@@ -215,16 +220,16 @@
       msgbuf.append('\n');
     }
 
-    if (!contains(footers, FooterConstants.CHANGE_ID, n.change().getKey().get())) {
+    if (!contains(footers, FooterConstants.CHANGE_ID, c.getKey().get())) {
       msgbuf.append(FooterConstants.CHANGE_ID.getName());
       msgbuf.append(": ");
-      msgbuf.append(n.change().getKey().get());
+      msgbuf.append(c.getKey().get());
       msgbuf.append('\n');
     }
 
     final String siteUrl = urlProvider.get();
     if (siteUrl != null) {
-      final String url = siteUrl + n.getPatchsetId().getParentKey().get();
+      final String url = siteUrl + c.getId().get();
       if (!contains(footers, FooterConstants.REVIEWED_ON, url)) {
         msgbuf.append(FooterConstants.REVIEWED_ON.getName());
         msgbuf.append(": ");
@@ -235,7 +240,7 @@
 
     PatchSetApproval submitAudit = null;
 
-    for (final PatchSetApproval a : safeGetApprovals(n)) {
+    for (final PatchSetApproval a : safeGetApprovals(ctl, psId)) {
       if (a.getValue() <= 0) {
         // Negative votes aren't counted.
         continue;
@@ -301,6 +306,10 @@
     return msgbuf.toString();
   }
 
+  public String createCherryPickCommitMessage(final CodeReviewCommit n) {
+    return createCherryPickCommitMessage(n, n.getControl(), n.getPatchsetId());
+  }
+
   private static boolean isCodeReview(LabelId id) {
     return "Code-Review".equalsIgnoreCase(id.get());
   }
@@ -309,11 +318,12 @@
     return "Verified".equalsIgnoreCase(id.get());
   }
 
-  private Iterable<PatchSetApproval> safeGetApprovals(CodeReviewCommit n) {
+  private Iterable<PatchSetApproval> safeGetApprovals(
+      ChangeControl ctl, PatchSet.Id psId) {
     try {
-      return approvalsUtil.byPatchSet(db.get(), n.getControl(), n.getPatchsetId());
+      return approvalsUtil.byPatchSet(db.get(), ctl, psId);
     } catch (OrmException e) {
-      log.error("Can't read approval records for " + n.getPatchsetId(), e);
+      log.error("Can't read approval records for " + psId, e);
       return Collections.emptyList();
     }
   }
@@ -667,7 +677,7 @@
       }
 
       @Override
-      public void release() {
+      public void close() {
       }
     });
     return (ThreeWayMerger) m;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java
new file mode 100644
index 0000000..0df866d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueryList.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public class QueryList extends TabFile {
+  public static final String FILE_NAME = "queries";
+  protected final Map<String, String> queriesByName;
+
+  private QueryList(List<Row> queriesByName) {
+    this.queriesByName = toMap(queriesByName);
+  }
+
+  public static QueryList parse(String text, ValidationError.Sink errors)
+      throws IOException {
+    return new QueryList(parse(text, FILE_NAME, errors));
+  }
+
+  public String getQuery(String name) {
+    return queriesByName.get(name);
+  }
+
+  public String asText() {
+    return asText("Name", "Query", queriesByName);
+  }
+}
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 0793d10..3fb515a 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
@@ -2367,9 +2367,12 @@
     walk.reset();
     walk.sort(RevSort.NONE);
     try {
-      walk.markStart(walk.parseCommit(cmd.getNewId()));
+      RevObject parsedObject = walk.parseAny(cmd.getNewId());
+      if (!(parsedObject instanceof RevCommit)) {
+        return;
+      }
+      walk.markStart((RevCommit)parsedObject);
       markHeadsAsUninteresting(walk, cmd.getRefName());
-
       Set<ObjectId> existing = changeRefsById().keySet();
       for (RevCommit c; (c = walk.next()) != null;) {
         if (existing.contains(c)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
index 25fbfb9..365000b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
@@ -25,6 +25,7 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 
@@ -48,9 +49,12 @@
   @Provides
   @Singleton
   @EmailReviewCommentsExecutor
-  public WorkQueue.Executor createEmailReviewCommentsExecutor(
+  public ExecutorService createEmailReviewCommentsExecutor(
       @GerritServerConfig Config config, WorkQueue queues) {
     int poolSize = config.getInt("sendemail", null, "threadPoolSize", 1);
+    if (poolSize == 0) {
+      return MoreExecutors.newDirectExecutorService();
+    }
     return queues.createQueue(poolSize, "EmailReviewComments");
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
index 2c78133..65808fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ScanningChangeCacheImpl.java
@@ -48,7 +48,7 @@
 @Singleton
 public class ScanningChangeCacheImpl implements ChangeCache {
   private static final Logger log =
-      LoggerFactory.getLogger(SearchingChangeCacheImpl.class);
+      LoggerFactory.getLogger(ScanningChangeCacheImpl.class);
 
   public static Module module() {
     return new CacheModule() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index ab18641..0f66da4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
@@ -61,6 +62,7 @@
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -154,42 +156,47 @@
     }
 
     try {
-      final TreeWalk tw = TreeWalk.forPath(db, GIT_MODULES, mergeTip.getTree());
-      if (tw != null
-          && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) || FileMode.EXECUTABLE_FILE
-              .equals(tw.getRawMode(0)))) {
+      Set<SubmoduleSubscription> oldSubscriptions =
+          Sets.newHashSet(schema.submoduleSubscriptions()
+              .bySuperProject(destBranch));
 
+      Set<SubmoduleSubscription> newSubscriptions;
+      TreeWalk tw = TreeWalk.forPath(db, GIT_MODULES, mergeTip.getTree());
+      if (tw != null
+          && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) ||
+              FileMode.EXECUTABLE_FILE.equals(tw.getRawMode(0)))) {
         BlobBasedConfig bbc =
             new BlobBasedConfig(null, db, mergeTip, GIT_MODULES);
 
-        final String thisServer = new URI(urlProvider.get()).getHost();
+        String thisServer = new URI(urlProvider.get()).getHost();
 
-        final Branch.NameKey target =
+        Branch.NameKey target =
             new Branch.NameKey(new Project.NameKey(destProject.getName()),
                 destBranch.get());
 
-        final Set<SubmoduleSubscription> oldSubscriptions =
-            new HashSet<>(schema.submoduleSubscriptions()
-                .bySuperProject(destBranch).toList());
-        List<SubmoduleSubscription> newSubscriptions =
-            subSecParserFactory.create(bbc, thisServer, target)
-                .parseAllSections();
+        newSubscriptions = subSecParserFactory.create(bbc, thisServer, target)
+            .parseAllSections();
+      } else {
+        newSubscriptions = Collections.emptySet();
+      }
 
-        final Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
-        for (SubmoduleSubscription s : newSubscriptions) {
-          if (oldSubscriptions.contains(s)) {
-            alreadySubscribeds.add(s);
-          }
+      Set<SubmoduleSubscription> alreadySubscribeds = new HashSet<>();
+      for (SubmoduleSubscription s : newSubscriptions) {
+        if (oldSubscriptions.contains(s)) {
+          alreadySubscribeds.add(s);
         }
+      }
 
-        oldSubscriptions.removeAll(newSubscriptions);
-        newSubscriptions.removeAll(alreadySubscribeds);
+      oldSubscriptions.removeAll(newSubscriptions);
+      newSubscriptions.removeAll(alreadySubscribeds);
 
-        if (!oldSubscriptions.isEmpty()) {
-          schema.submoduleSubscriptions().delete(oldSubscriptions);
-        }
+      if (!oldSubscriptions.isEmpty()) {
+        schema.submoduleSubscriptions().delete(oldSubscriptions);
+      }
+      if (!newSubscriptions.isEmpty()) {
         schema.submoduleSubscriptions().insert(newSubscriptions);
       }
+
     } catch (OrmException e) {
       logAndThrowSubmoduleException(
           "Database problem at update of subscriptions table from "
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index ecf53fd..4a8163b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -110,8 +110,8 @@
     return defaultQueue;
   }
 
-  /** Create a new executor queue with one thread. */
-  public Executor createQueue(final int poolsize, final String prefix) {
+  /** Create a new executor queue. */
+  public Executor createQueue(int poolsize, String prefix) {
     final Executor r = new Executor(poolsize, prefix);
     r.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
     r.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index e6fff19..dd981ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -89,7 +89,7 @@
                     .getSubmitter(n).getAccountId());
             PatchSet newPatchSet =
                 rebaseChange.rebase(args.repo, args.rw, args.inserter,
-                    n.getPatchsetId(), n.change(), uploader,
+                    n.change(), n.getPatchsetId(), uploader,
                     mergeTip.getCurrentTip(), args.mergeUtil,
                     args.serverIdent.get(), false, ValidatePolicy.NONE);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 43b8c8d..9054ba4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -266,7 +266,7 @@
             throws OrmException {
           Change c = input.change();
           if (c == null) {
-            return null;
+            return ImmutableSet.of();
           }
           Set<Integer> r = Sets.newHashSet();
           if (!args.allowsDrafts && c.getStatus() == Change.Status.DRAFT) {
@@ -287,7 +287,7 @@
         public Iterable<String> get(ChangeData input, FillArgs args)
             throws OrmException {
           Set<String> revisions = Sets.newHashSet();
-          for (PatchSet ps : input.patches()) {
+          for (PatchSet ps : input.patchSets()) {
             if (ps.getRevision() != null) {
               revisions.add(ps.getRevision().get());
             }
@@ -306,7 +306,7 @@
           try {
             List<FooterLine> footers = input.commitFooters();
             if (footers == null) {
-              return null;
+              return ImmutableSet.of();
             }
             return Sets.newHashSet(
                 args.trackingFooters.extract(footers).values());
@@ -531,6 +531,25 @@
         }
       };
 
+  public static class PatchSetProtoField
+      extends FieldDef.Repeatable<ChangeData, byte[]> {
+    public static final ProtobufCodec<PatchSet> CODEC =
+        CodecFactory.encoder(PatchSet.class);
+
+    private PatchSetProtoField() {
+      super("_patch_set", FieldType.STORED_ONLY, true);
+    }
+
+    @Override
+    public Iterable<byte[]> get(ChangeData input, FieldDef.FillArgs args)
+        throws OrmException {
+      return toProtos(CODEC, input.patchSets());
+    }
+  }
+
+  /** Serialized patch set object, used for pre-populating results. */
+  public static final PatchSetProtoField PATCH_SET = new PatchSetProtoField();
+
   private static String getTopic(ChangeData input) throws OrmException {
     Change c = input.change();
     if (c == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index 46c8297..cff654a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -173,6 +173,35 @@
       ChangeField.HASHTAG,
       ChangeField.COMMENTBY);
 
+  static final Schema<ChangeData> V17 = schema(
+      ChangeField.LEGACY_ID,
+      ChangeField.ID,
+      ChangeField.STATUS,
+      ChangeField.PROJECT,
+      ChangeField.PROJECTS,
+      ChangeField.REF,
+      ChangeField.TOPIC,
+      ChangeField.UPDATED,
+      ChangeField.FILE_PART,
+      ChangeField.PATH,
+      ChangeField.OWNER,
+      ChangeField.REVIEWER,
+      ChangeField.COMMIT,
+      ChangeField.TR,
+      ChangeField.LABEL,
+      ChangeField.REVIEWED,
+      ChangeField.COMMIT_MESSAGE,
+      ChangeField.COMMENT,
+      ChangeField.CHANGE,
+      ChangeField.APPROVAL,
+      ChangeField.MERGEABLE,
+      ChangeField.ADDED,
+      ChangeField.DELETED,
+      ChangeField.DELTA,
+      ChangeField.HASHTAG,
+      ChangeField.COMMENTBY,
+      ChangeField.PATCH_SET);
+
   private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<>(ImmutableList.copyOf(fields));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index 41df287..0cfc659 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -114,7 +114,6 @@
   @Singleton
   @IndexExecutor(BATCH)
   ListeningExecutorService getBatchIndexExecutor(
-      @IndexExecutor(INTERACTIVE) ListeningExecutorService interactive,
       @GerritServerConfig Config config,
       WorkQueue workQueue) {
     if (batchExecutor != null) {
@@ -125,7 +124,7 @@
       threads = config.getInt("changeMerge", null, "threadPoolSize", 0);
     }
     if (threads <= 0) {
-      return interactive;
+      threads = Runtime.getRuntime().availableProcessors();
     }
     return MoreExecutors.listeningDecorator(
         workQueue.createQueue(threads, "Index-Batch"));
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 fc6eadd..8147cff 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
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import static com.google.gerrit.server.PatchLineCommentsUtil.getCommentPsId;
+
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
 import com.google.common.collect.Ordering;
@@ -175,7 +177,8 @@
     short side = comment.getSide();
     CommentRange range = comment.getRange();
     if (range != null) {
-      String prefix = String.format("Line %d: ", range.getStartLine());
+      String prefix = "PS" + getCommentPsId(comment).get()
+        + ", Line " + range.getStartLine() + ": ";
       for (int n = range.getStartLine(); n <= range.getEndLine(); n++) {
         out.append(n == range.getStartLine()
             ? prefix
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 3b51bb4..47c1731 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -16,14 +16,14 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
+import static com.google.gerrit.server.notedb.CommentsInNotesUtil.addCommentToMap;
 
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Table;
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.client.RevId;
@@ -46,8 +46,12 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -165,11 +169,6 @@
   }
 
   private void verifyComment(PatchLineComment comment) {
-    checkState(psId != null,
-        "setPatchSetId must be called first");
-    checkArgument(getCommentPsId(comment).equals(psId),
-        "Comment on %s does not match configured patch set %s",
-        getCommentPsId(comment), psId);
     if (migration.writeChanges()) {
       checkArgument(comment.getRevId() != null);
     }
@@ -190,76 +189,55 @@
       noteMap = NoteMap.newEmptyMap();
     }
 
-    Table<PatchSet.Id, String, PatchLineComment> baseDrafts =
-        draftNotes.getDraftBaseComments();
-    Table<PatchSet.Id, String, PatchLineComment> psDrafts =
-        draftNotes.getDraftPsComments();
+    Map<RevId, List<PatchLineComment>> allComments = new HashMap<>();
 
-    boolean draftsEmpty = baseDrafts.isEmpty() && psDrafts.isEmpty();
-
-    // There is no need to rewrite the note for one of the sides of the patch
-    // set if all of the modifications were made to the comments of one side,
-    // so we set these flags to potentially save that extra work.
-    boolean baseSideChanged = false;
-    boolean revisionSideChanged = false;
-
-    // We must define these RevIds so that if this update deletes all
-    // remaining comments on a given side, then we can remove that note.
-    // However, if this update doesn't delete any comments, it is okay for these
-    // to be null because they won't be used.
-    RevId baseRevId = null;
-    RevId psRevId = null;
-
+    boolean hasComments = false;
+    int n = deleteComments.size() + upsertComments.size();
+    Set<RevId> updatedRevs = Sets.newHashSetWithExpectedSize(n);
+    Set<PatchLineComment.Key> updatedKeys = Sets.newHashSetWithExpectedSize(n);
     for (PatchLineComment c : deleteComments) {
-      if (c.getSide() == (short) 0) {
-        baseSideChanged = true;
-        baseRevId = c.getRevId();
-        baseDrafts.remove(psId, c.getKey().get());
-      } else {
-        revisionSideChanged = true;
-        psRevId = c.getRevId();
-        psDrafts.remove(psId, c.getKey().get());
-      }
+      allComments.put(c.getRevId(), new ArrayList<PatchLineComment>());
+      updatedRevs.add(c.getRevId());
+      updatedKeys.add(c.getKey());
     }
 
     for (PatchLineComment c : upsertComments) {
-      if (c.getSide() == (short) 0) {
-        baseSideChanged = true;
-        baseDrafts.put(psId, c.getKey().get(), c);
-      } else {
-        revisionSideChanged = true;
-        psDrafts.put(psId, c.getKey().get(), c);
+      hasComments = true;
+      addCommentToMap(allComments, c);
+      updatedRevs.add(c.getRevId());
+      updatedKeys.add(c.getKey());
+    }
+
+    // Re-add old comments for updated revisions so the new note contents
+    // includes both old and new comments merged in the right order.
+    //
+    // writeCommentsToNoteMap doesn't touch notes for SHA-1s that are not
+    // mentioned in the input map, so by omitting comments for those revisions,
+    // we avoid the work of having to re-serialize identical comment data for
+    // those revisions.
+    ListMultimap<RevId, PatchLineComment> existing =
+        draftNotes.getComments();
+    for (Map.Entry<RevId, PatchLineComment> e : existing.entries()) {
+      PatchLineComment c = e.getValue();
+      if (updatedRevs.contains(c.getRevId())
+          && !updatedKeys.contains(c.getKey())) {
+        hasComments = true;
+        addCommentToMap(allComments, e.getValue());
       }
     }
 
-    List<PatchLineComment> newBaseDrafts =
-        Lists.newArrayList(baseDrafts.row(psId).values());
-    List<PatchLineComment> newPsDrafts =
-        Lists.newArrayList(psDrafts.row(psId).values());
+    // If we touched every revision and there are no comments left, set the flag
+    // for the caller to delete the entire ref.
+    boolean touchedAllRevs = updatedRevs.equals(existing.keySet());
+    if (touchedAllRevs && !hasComments) {
+      removedAllComments.set(touchedAllRevs && !hasComments);
+      return null;
+    }
 
-    updateNoteMap(baseSideChanged, noteMap, newBaseDrafts,
-        baseRevId);
-    updateNoteMap(revisionSideChanged, noteMap, newPsDrafts,
-        psRevId);
-
-    removedAllComments.set(
-        baseDrafts.isEmpty() && psDrafts.isEmpty() && !draftsEmpty);
-
+    commentsUtil.writeCommentsToNoteMap(noteMap, allComments, inserter);
     return noteMap.writeTree(inserter);
   }
 
-  private void updateNoteMap(boolean changed, NoteMap noteMap,
-      List<PatchLineComment> comments, RevId commitId)
-      throws IOException {
-    if (changed) {
-      if (comments.isEmpty()) {
-        commentsUtil.removeNote(noteMap, commitId);
-      } else {
-        commentsUtil.writeCommentsToNoteMap(noteMap, comments, inserter);
-      }
-    }
-  }
-
   public RevCommit commit() throws IOException {
     BatchMetaDataUpdate batch = openUpdate();
     try {
@@ -279,13 +257,11 @@
     if (migration.writeChanges()) {
       AtomicBoolean removedAllComments = new AtomicBoolean();
       ObjectId treeId = storeCommentsInNotes(removedAllComments);
-      if (treeId != null) {
-        if (removedAllComments.get()) {
-          batch.removeRef(getRefName());
-        } else {
-          builder.setTreeId(treeId);
-          batch.write(builder);
-        }
+      if (removedAllComments.get()) {
+        batch.removeRef(getRefName());
+      } else if (treeId != null) {
+        builder.setTreeId(treeId);
+        batch.write(builder);
       }
     }
   }
@@ -308,7 +284,7 @@
     }
     commit.setAuthor(newIdent(getUser().getAccount(), when));
     commit.setCommitter(new PersonIdent(serverIdent, when));
-    commit.setMessage(String.format("Comment on patch set %d", psId.get()));
+    commit.setMessage("Update draft comments");
     return true;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 6d4f3a4..063ff5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -15,18 +15,15 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
-import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Ordering;
-import com.google.common.collect.Table;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -36,6 +33,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -52,8 +50,6 @@
 
 import java.io.IOException;
 import java.sql.Timestamp;
-import java.util.Comparator;
-import java.util.List;
 import java.util.Map;
 
 /** View of a single {@link Change} based on the log of its notes branch. */
@@ -76,20 +72,6 @@
           }
         });
 
-  public static Comparator<PatchLineComment> PatchLineCommentComparator =
-      new Comparator<PatchLineComment>() {
-    @Override
-    public int compare(PatchLineComment c1, PatchLineComment c2) {
-      String filename1 = c1.getKey().getParentKey().get();
-      String filename2 = c2.getKey().getParentKey().get();
-      return ComparisonChain.start()
-          .compare(filename1, filename2)
-          .compare(c1.getLine(), c2.getLine())
-          .compare(c1.getWrittenOn(), c2.getWrittenOn())
-          .result();
-    }
-  };
-
   public static ConfigInvalidException parseException(Change.Id changeId,
       String fmt, Object... args) {
     return new ConfigInvalidException("Change " + changeId + ": "
@@ -138,8 +120,7 @@
   private ImmutableList<Account.Id> allPastReviewers;
   private ImmutableList<SubmitRecord> submitRecords;
   private ImmutableListMultimap<PatchSet.Id, ChangeMessage> changeMessages;
-  private ImmutableListMultimap<PatchSet.Id, PatchLineComment> commentsForBase;
-  private ImmutableListMultimap<PatchSet.Id, PatchLineComment> commentsForPS;
+  private ImmutableListMultimap<RevId, PatchLineComment> comments;
   private ImmutableSet<String> hashtags;
   NoteMap noteMap;
 
@@ -194,28 +175,15 @@
     return changeMessages;
   }
 
-  /** @return inline comments on each patchset's base (side == 0). */
-  public ImmutableListMultimap<PatchSet.Id, PatchLineComment>
-      getBaseComments() {
-    return commentsForBase;
+  /** @return inline comments on each revision. */
+  public ImmutableListMultimap<RevId, PatchLineComment> getComments() {
+    return comments;
   }
 
-  /** @return inline comments on each patchset (side == 1). */
-  public ImmutableListMultimap<PatchSet.Id, PatchLineComment>
-      getPatchSetComments() {
-    return commentsForPS;
-  }
-
-  public Table<PatchSet.Id, String, PatchLineComment> getDraftBaseComments(
+  public ImmutableListMultimap<RevId, PatchLineComment> getDraftComments(
       Account.Id author) throws OrmException {
     loadDraftComments(author);
-    return draftCommentNotes.getDraftBaseComments();
-  }
-
-  public Table<PatchSet.Id, String, PatchLineComment> getDraftPsComments(
-      Account.Id author) throws OrmException {
-    loadDraftComments(author);
-    return draftCommentNotes.getDraftPsComments();
+    return draftCommentNotes.getComments();
   }
 
   /**
@@ -234,6 +202,11 @@
     }
   }
 
+  @VisibleForTesting
+  DraftCommentNotes getDraftCommentNotes() {
+    return draftCommentNotes;
+  }
+
   public boolean containsComment(PatchLineComment c) throws OrmException {
     if (containsCommentPublished(c)) {
       return true;
@@ -243,11 +216,7 @@
   }
 
   public boolean containsCommentPublished(PatchLineComment c) {
-    PatchSet.Id psId = getCommentPsId(c);
-    List<PatchLineComment> list = (c.getSide() == (short) 0)
-        ? getBaseComments().get(psId)
-        : getPatchSetComments().get(psId);
-    for (PatchLineComment l : list) {
+    for (PatchLineComment l : getComments().values()) {
       if (c.getKey().equals(l.getKey())) {
         return true;
       }
@@ -282,8 +251,7 @@
       }
       approvals = parser.buildApprovals();
       changeMessages = parser.buildMessages();
-      commentsForBase = ImmutableListMultimap.copyOf(parser.commentsForBase);
-      commentsForPS = ImmutableListMultimap.copyOf(parser.commentsForPs);
+      comments = ImmutableListMultimap.copyOf(parser.comments);
       noteMap = parser.commentNoteMap;
 
       if (parser.hashtags != null) {
@@ -310,8 +278,7 @@
     reviewers = ImmutableSetMultimap.of();
     submitRecords = ImmutableList.of();
     changeMessages = ImmutableListMultimap.of();
-    commentsForBase = ImmutableListMultimap.of();
-    commentsForPS = ImmutableListMultimap.of();
+    comments = ImmutableListMultimap.of();
     hashtags = ImmutableSet.of();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index b5b3c74..6f8cb2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -44,6 +44,7 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.util.LabelVote;
 
@@ -72,8 +73,7 @@
   final Map<Account.Id, ReviewerState> reviewers;
   final List<Account.Id> allPastReviewers;
   final List<SubmitRecord> submitRecords;
-  final Multimap<PatchSet.Id, PatchLineComment> commentsForPs;
-  final Multimap<PatchSet.Id, PatchLineComment> commentsForBase;
+  final Multimap<RevId, PatchLineComment> comments;
   NoteMap commentNoteMap;
   Change.Status status;
   Set<String> hashtags;
@@ -99,8 +99,7 @@
     allPastReviewers = Lists.newArrayList();
     submitRecords = Lists.newArrayListWithExpectedSize(1);
     changeMessages = LinkedListMultimap.create();
-    commentsForPs = ArrayListMultimap.create();
-    commentsForBase = ArrayListMultimap.create();
+    comments = ArrayListMultimap.create();
   }
 
   @Override
@@ -275,7 +274,7 @@
       throws IOException, ConfigInvalidException {
     commentNoteMap = CommentsInNotesUtil.parseCommentsFromNotes(repo,
         ChangeNoteUtil.changeRefName(changeId), walk, changeId,
-        commentsForBase, commentsForPs, PatchLineComment.Status.PUBLISHED);
+        comments, PatchLineComment.Status.PUBLISHED);
   }
 
   private void parseApproval(PatchSet.Id psId, Account.Id accountId,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
index 76dfdc8..d715947 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeRebuilder.java
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ArrayListMultimap;
@@ -33,6 +32,7 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
@@ -306,7 +306,7 @@
 
     PatchLineCommentEvent(PatchLineComment c, Change change, PatchSet ps,
         PatchListCache cache) {
-      super(getCommentPsId(c), c.getAuthor(), c.getWrittenOn());
+      super(PatchLineCommentsUtil.getCommentPsId(c), c.getAuthor(), c.getWrittenOn());
       this.c = c;
       this.change = change;
       this.ps = ps;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 7302425..43c232b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -20,7 +20,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH;
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
+import static com.google.gerrit.server.notedb.CommentsInNotesUtil.addCommentToMap;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Joiner;
@@ -28,14 +28,13 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
 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.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AnonymousCowardName;
@@ -57,6 +56,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.List;
@@ -91,8 +91,7 @@
   private String subject;
   private List<SubmitRecord> submitRecords;
   private final CommentsInNotesUtil commentsUtil;
-  private List<PatchLineComment> commentsForBase;
-  private List<PatchLineComment> commentsForPs;
+  private List<PatchLineComment> comments;
   private Set<String> hashtags;
   private String changeMessage;
   private ChangeNotes notes;
@@ -161,8 +160,7 @@
     this.commentsUtil = commentsUtil;
     this.approvals = Maps.newTreeMap(labelNameComparator);
     this.reviewers = Maps.newLinkedHashMap();
-    this.commentsForPs = Lists.newArrayList();
-    this.commentsForBase = Lists.newArrayList();
+    this.comments = Lists.newArrayList();
   }
 
   public void setStatus(Change.Status status) {
@@ -238,15 +236,11 @@
           "A comment already exists with the same key as the following comment,"
           + " so we cannot insert this comment: %s", c);
     }
-    if (c.getSide() == 0) {
-      commentsForBase.add(c);
-    } else {
-      commentsForPs.add(c);
-    }
+    comments.add(c);
   }
 
   private void insertDraftComment(PatchLineComment c) throws OrmException {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.insertComment(c);
   }
 
@@ -263,15 +257,11 @@
       checkArgument(!notes.containsCommentPublished(c),
           "Cannot update a comment that has already been published and saved");
     }
-    if (c.getSide() == 0) {
-      commentsForBase.add(c);
-    } else {
-      commentsForPs.add(c);
-    }
+    comments.add(c);
   }
 
   private void upsertDraftComment(PatchLineComment c) {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.upsertComment(c);
   }
 
@@ -286,46 +276,32 @@
       checkArgument(!notes.containsCommentPublished(c),
           "Cannot update a comment that has already been published and saved");
     }
-    if (c.getSide() == 0) {
-      commentsForBase.add(c);
-    } else {
-      commentsForPs.add(c);
-    }
+    comments.add(c);
   }
 
   private void updateDraftComment(PatchLineComment c) throws OrmException {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.updateComment(c);
   }
 
   private void deleteDraftComment(PatchLineComment c) throws OrmException {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.deleteComment(c);
   }
 
   private void deleteDraftCommentIfPresent(PatchLineComment c)
       throws OrmException {
-    createDraftUpdateIfNull(c);
+    createDraftUpdateIfNull();
     draftUpdate.deleteCommentIfPresent(c);
   }
 
-  private void createDraftUpdateIfNull(PatchLineComment c) {
+  private void createDraftUpdateIfNull() {
     if (draftUpdate == null) {
       draftUpdate = draftUpdateFactory.create(ctl, when);
-      if (psId != null) {
-        draftUpdate.setPatchSetId(psId);
-      } else {
-        draftUpdate.setPatchSetId(getCommentPsId(c));
-      }
     }
   }
 
   private void verifyComment(PatchLineComment c) {
-    checkArgument(psId != null,
-        "setPatchSetId must be called first");
-    checkArgument(getCommentPsId(c).equals(psId),
-        "Comment on %s doesn't match previous patch set %s",
-        getCommentPsId(c), psId);
     checkArgument(c.getRevId() != null);
     checkArgument(c.getStatus() == Status.PUBLISHED,
         "Cannot add a draft comment to a ChangeUpdate. Use a ChangeDraftUpdate"
@@ -356,31 +332,23 @@
     if (noteMap == null) {
       noteMap = NoteMap.newEmptyMap();
     }
-    if (commentsForPs.isEmpty() && commentsForBase.isEmpty()) {
+    if (comments.isEmpty()) {
       return null;
     }
 
-    Multimap<PatchSet.Id, PatchLineComment> allCommentsOnBases =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> allCommentsOnPs =
-        notes.getPatchSetComments();
-
-    // This writes all comments for the base of this PS to the note map.
-    if (!commentsForBase.isEmpty()) {
-      List<PatchLineComment> baseCommentsForThisPs =
-          new ArrayList<>(allCommentsOnBases.get(psId));
-      baseCommentsForThisPs.addAll(commentsForBase);
-      commentsUtil.writeCommentsToNoteMap(noteMap, baseCommentsForThisPs,
-          inserter);
+    Map<RevId, List<PatchLineComment>> allComments = Maps.newHashMap();
+    for (Map.Entry<RevId, Collection<PatchLineComment>> e
+        : notes.getComments().asMap().entrySet()) {
+      List<PatchLineComment> comments = new ArrayList<>();
+      for (PatchLineComment c : e.getValue()) {
+        comments.add(c);
+      }
+      allComments.put(e.getKey(), comments);
     }
-
-    // This write all comments for this PS to the note map.
-    if (!commentsForPs.isEmpty()) {
-      List<PatchLineComment> commentsForThisPs =
-          new ArrayList<>(allCommentsOnPs.get(psId));
-      commentsForThisPs.addAll(commentsForPs);
-      commentsUtil.writeCommentsToNoteMap(noteMap, commentsForThisPs, inserter);
+    for (PatchLineComment c : comments) {
+      addCommentToMap(allComments, c);
     }
+    commentsUtil.writeCommentsToNoteMap(noteMap, allComments, inserter);
     return noteMap.writeTree(inserter);
   }
 
@@ -504,8 +472,7 @@
   private boolean isEmpty() {
     return approvals.isEmpty()
         && changeMessage == null
-        && commentsForBase.isEmpty()
-        && commentsForPs.isEmpty()
+        && comments.isEmpty()
         && reviewers.isEmpty()
         && status == null
         && subject == null
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
index 34ecbc0..149325d9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/CommentsInNotesUtil.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.PatchLineCommentsUtil.PLC_ORDER;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.GERRIT_PLACEHOLDER_HOST;
 import static com.google.gerrit.server.notedb.ChangeNotes.parseException;
 import static java.nio.charset.StandardCharsets.UTF_8;
@@ -33,6 +34,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.PatchLineCommentsUtil;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
@@ -63,9 +65,11 @@
 import java.nio.charset.Charset;
 import java.sql.Timestamp;
 import java.text.ParseException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Utility functions to parse PatchLineComments out of a note byte array and
@@ -86,8 +90,7 @@
 
   public static NoteMap parseCommentsFromNotes(Repository repo, String refName,
       RevWalk walk, Change.Id changeId,
-      Multimap<PatchSet.Id, PatchLineComment> commentsForBase,
-      Multimap<PatchSet.Id, PatchLineComment> commentsForPs,
+      Multimap<RevId, PatchLineComment> comments,
       Status status)
       throws IOException, ConfigInvalidException {
     Ref ref = repo.getRef(refName);
@@ -99,20 +102,14 @@
     RevCommit commit = walk.parseCommit(ref.getObjectId());
     NoteMap noteMap = NoteMap.read(reader, commit);
 
-    for (Note note: noteMap) {
+    for (Note note : noteMap) {
       byte[] bytes =
           reader.open(note.getData(), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
       List<PatchLineComment> result = parseNote(bytes, changeId, status);
       if (result == null || result.isEmpty()) {
         continue;
       }
-      PatchSet.Id psId = result.get(0).getKey().getParentKey().getParentKey();
-      short side = result.get(0).getSide();
-      if (side == 0) {
-        commentsForBase.putAll(psId, result);
-      } else {
-        commentsForPs.putAll(psId, result);
-      }
+      comments.putAll(new RevId(note.name()), result);
     }
     return noteMap;
   }
@@ -152,10 +149,6 @@
     return dateFormatter.formatDate(newIdent);
   }
 
-  public static PatchSet.Id getCommentPsId(PatchLineComment plc) {
-    return plc.getKey().getParentKey().getParentKey();
-  }
-
   private static PatchLineComment parseComment(byte[] note, MutableInteger curr,
       String currentFileName, PatchSet.Id psId, RevId revId, boolean isForBase,
       Charset enc, Status status)
@@ -449,7 +442,7 @@
     PatchLineComment first = comments.get(0);
 
     short side = first.getSide();
-    PatchSet.Id psId = getCommentPsId(first);
+    PatchSet.Id psId = PatchLineCommentsUtil.getCommentPsId(first);
     appendHeaderField(writer, side == 0
         ? BASE_PATCH_SET
         : PATCH_SET,
@@ -459,7 +452,7 @@
     String currentFilename = null;
 
     for (PatchLineComment c : comments) {
-      PatchSet.Id currentPsId = getCommentPsId(c);
+      PatchSet.Id currentPsId = PatchLineCommentsUtil.getCommentPsId(c);
       checkArgument(psId.equals(currentPsId),
           "All comments being added must all have the same PatchSet.Id. The"
           + "comment below does not have the same PatchSet.Id as the others "
@@ -524,19 +517,47 @@
     return buf.toByteArray();
   }
 
+  /**
+   * Write comments for multiple revisions to a note map.
+   * <p>
+   * Mutates the map in-place. only notes for SHA-1s found as keys in the map
+   * are modified; all other notes are left untouched.
+   *
+   * @param noteMap note map to modify.
+   * @param allComments map of revision to all comments for that revision;
+   *     callers are responsible for reading the original comments and applying
+   *     any changes. Differs from a multimap in that present-but-empty values
+   *     are significant, and indicate the note for that SHA-1 should be
+   *     deleted.
+   * @param inserter object inserter for writing notes.
+   * @throws IOException if an error occurred.
+   */
   public void writeCommentsToNoteMap(NoteMap noteMap,
-      List<PatchLineComment> allComments, ObjectInserter inserter)
-        throws IOException {
-    checkArgument(!allComments.isEmpty(),
-        "No comments to write; to delete, use removeNoteFromNoteMap().");
-    ObjectId commit =
-        ObjectId.fromString(allComments.get(0).getRevId().get());
-    Collections.sort(allComments, ChangeNotes.PatchLineCommentComparator);
-    noteMap.set(commit, inserter.insert(OBJ_BLOB, buildNote(allComments)));
+      Map<RevId, List<PatchLineComment>> allComments, ObjectInserter inserter)
+      throws IOException {
+    for (Map.Entry<RevId, List<PatchLineComment>> e : allComments.entrySet()) {
+      List<PatchLineComment> comments = e.getValue();
+      ObjectId commit = ObjectId.fromString(e.getKey().get());
+      if (comments.isEmpty()) {
+        noteMap.remove(commit);
+        continue;
+      }
+      Collections.sort(comments, PLC_ORDER);
+      // We allow comments for multiple commits to be written in the same
+      // update, even though the rest of the metadata update is associated with
+      // a single patch set.
+      noteMap.set(commit, inserter.insert(OBJ_BLOB, buildNote(comments)));
+    }
   }
 
-  public void removeNote(NoteMap noteMap, RevId commitId)
-      throws IOException {
-    noteMap.remove(ObjectId.fromString(commitId.get()));
+  static void addCommentToMap(Map<RevId, List<PatchLineComment>> map,
+      PatchLineComment c) {
+    List<PatchLineComment> list = map.get(c.getRevId());
+    if (list == null) {
+      list = new ArrayList<>();
+      map.put(c.getRevId(), list);
+    }
+    list.add(c);
   }
+
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 20d6c4a..a02c24d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -14,18 +14,14 @@
 
 package com.google.gerrit.server.notedb;
 
-import static com.google.gerrit.server.notedb.CommentsInNotesUtil.getCommentPsId;
-
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.HashBasedTable;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Table;
+import com.google.common.collect.ImmutableListMultimap;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -70,8 +66,7 @@
   private final AllUsersName draftsProject;
   private final Account.Id author;
 
-  private final Table<PatchSet.Id, String, PatchLineComment> draftBaseComments;
-  private final Table<PatchSet.Id, String, PatchLineComment> draftPsComments;
+  private ImmutableListMultimap<RevId, PatchLineComment> comments;
   private NoteMap noteMap;
 
   DraftCommentNotes(GitRepositoryManager repoManager, NotesMigration migration,
@@ -79,9 +74,6 @@
     super(repoManager, migration, changeId);
     this.draftsProject = draftsProject;
     this.author = author;
-
-    this.draftBaseComments = HashBasedTable.create();
-    this.draftPsComments = HashBasedTable.create();
   }
 
   public NoteMap getNoteMap() {
@@ -92,32 +84,18 @@
     return author;
   }
 
-  /**
-   * @return a defensive copy of the table containing all draft comments
-   *    on this change with side == 0. The row key is the comment's PatchSet.Id,
-   *    the column key is the comment's UUID, and the value is the comment.
-   */
-  public Table<PatchSet.Id, String, PatchLineComment>
-      getDraftBaseComments() {
-    return HashBasedTable.create(draftBaseComments);
-  }
-
-  /**
-   * @return a defensive copy of the table containing all draft comments
-   *    on this change with side == 1. The row key is the comment's PatchSet.Id,
-   *    the column key is the comment's UUID, and the value is the comment.
-   */
-  public Table<PatchSet.Id, String, PatchLineComment>
-      getDraftPsComments() {
-    return HashBasedTable.create(draftPsComments);
+  public ImmutableListMultimap<RevId, PatchLineComment> getComments() {
+    // TODO(dborowitz): Defensive copy?
+    return comments;
   }
 
   public boolean containsComment(PatchLineComment c) {
-    Table<PatchSet.Id, String, PatchLineComment> t =
-        c.getSide() == (short) 0
-        ? getDraftBaseComments()
-        : getDraftPsComments();
-    return t.contains(getCommentPsId(c), c.getKey().get());
+    for (PatchLineComment existing : comments.values()) {
+      if (c.getKey().equals(existing.getKey())) {
+        return true;
+      }
+    }
+    return false;
   }
 
   @Override
@@ -129,6 +107,7 @@
   protected void onLoad() throws IOException, ConfigInvalidException {
     ObjectId rev = getRevision();
     if (rev == null) {
+      loadDefaults();
       return;
     }
 
@@ -137,8 +116,7 @@
           getChangeId(), walk, rev, repoManager, draftsProject, author)) {
       parser.parseDraftComments();
 
-      buildCommentTable(draftBaseComments, parser.draftBaseComments);
-      buildCommentTable(draftPsComments, parser.draftPsComments);
+      comments = ImmutableListMultimap.copyOf(parser.comments);
       noteMap = parser.noteMap;
     }
   }
@@ -152,20 +130,11 @@
 
   @Override
   protected void loadDefaults() {
-    // Do nothing; tables are final and initialized in constructor.
+    comments = ImmutableListMultimap.of();
   }
 
   @Override
   protected Project.NameKey getProjectName() {
     return draftsProject;
   }
-
-  private void buildCommentTable(
-      Table<PatchSet.Id, String, PatchLineComment> commentTable,
-      Multimap<PatchSet.Id, PatchLineComment> allComments) {
-    for (PatchLineComment c : allComments.values()) {
-      commentTable.put(getCommentPsId(c), c.getKey().get(), c);
-    }
-  }
-
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java
index 4b3fbdf..ef8683f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotesParser.java
@@ -19,8 +19,8 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 
@@ -34,8 +34,7 @@
 import java.io.IOException;
 
 class DraftCommentNotesParser implements AutoCloseable {
-  final Multimap<PatchSet.Id, PatchLineComment> draftBaseComments;
-  final Multimap<PatchSet.Id, PatchLineComment> draftPsComments;
+  final Multimap<RevId, PatchLineComment> comments;
   NoteMap noteMap;
 
   private final Change.Id changeId;
@@ -53,8 +52,7 @@
     this.repo = repoManager.openMetadataRepository(draftsProject);
     this.author = author;
 
-    draftBaseComments = ArrayListMultimap.create();
-    draftPsComments = ArrayListMultimap.create();
+    comments = ArrayListMultimap.create();
   }
 
   @Override
@@ -66,7 +64,6 @@
     walk.markStart(walk.parseCommit(tip));
     noteMap = CommentsInNotesUtil.parseCommentsFromNotes(repo,
         RefNames.refsDraftComments(author, changeId),
-        walk, changeId, draftBaseComments,
-        draftPsComments, PatchLineComment.Status.DRAFT);
+        walk, changeId, comments, PatchLineComment.Status.DRAFT);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index c381261..b8caa62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -338,7 +338,7 @@
         }
 
         @Override
-        public void release() {
+        public void close() {
         }
       });
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index edbd9eb..5b70730 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -214,7 +214,9 @@
         a.displayMethod, b.displayMethod, a.mimeType.toString(),
         b.mimeType.toString(), comments, history, hugeFile,
         intralineDifferenceIsPossible, intralineFailure, intralineTimeout,
-        content.getPatchType() == Patch.PatchType.BINARY);
+        content.getPatchType() == Patch.PatchType.BINARY,
+        aId == null ? null : aId.getName(),
+        bId == null ? null : bId.getName());
   }
 
   private static boolean isModify(PatchListEntry content) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index dcfb52c..926ef44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -142,7 +142,7 @@
           new URLClassLoader(urls.toArray(new URL[urls.size()]),
               PluginLoader.parentFor(type));
 
-      JarScanner jarScanner = createJarScanner(srcJar);
+      JarScanner jarScanner = createJarScanner(tmp);
       ServerPlugin plugin = new ServerPlugin(name, description.canonicalUrl,
           description.user, srcJar, snapshot, jarScanner,
           description.dataDir, pluginLoader);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 56233f0..4e651c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -217,7 +217,8 @@
   private synchronized void unloadPlugin(Plugin plugin) {
     persistentCacheFactory.onStop(plugin);
     String name = plugin.getName();
-    log.info(String.format("Unloading plugin %s", name));
+    log.info(String.format("Unloading plugin %s, version %s",
+        name, plugin.getVersion()));
     plugin.stop(env);
     env.onStopPlugin(plugin);
     running.remove(name);
@@ -355,7 +356,9 @@
         String name = active.getName();
         try {
           log.info(String.format("Reloading plugin %s", name));
-          runPlugin(name, active.getSrcFile(), active);
+          Plugin newPlugin = runPlugin(name, active.getSrcFile(), active);
+          log.info(String.format("Reloaded plugin %s, version %s",
+              newPlugin.getName(), newPlugin.getVersion()));
         } catch (PluginInstallException e) {
           log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
           throw e;
@@ -395,14 +398,14 @@
       }
 
       if (active != null) {
-        log.info(String.format("Reloading plugin %s, version %s",
-            active.getName(), active.getVersion()));
+        log.info(String.format("Reloading plugin %s", active.getName()));
       }
 
       try {
         Plugin loadedPlugin = runPlugin(name, path, active);
-        if (active == null && !loadedPlugin.isDisabled()) {
-          log.info(String.format("Loaded plugin %s, version %s",
+        if (!loadedPlugin.isDisabled()) {
+          log.info(String.format("%s plugin %s, version %s",
+              active == null ? "Loaded" : "Reloaded",
               loadedPlugin.getName(), loadedPlugin.getVersion()));
         }
       } catch (PluginInstallException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 9cf7ce0..0abffde 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -212,7 +212,7 @@
             projectControlFactory.controlFor(p.getNameKey(), currentUser.get());
         PutConfig.Input in = new PutConfig.Input();
         in.pluginConfigValues = input.pluginConfigValues;
-        putConfig.get().apply(new ProjectResource(projectControl), in);
+        putConfig.get().apply(projectControl, in);
       } catch (NoSuchProjectException e) {
         throw new ResourceNotFoundException(p.getName());
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index fdff1e7..d451b46 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -32,7 +32,7 @@
    * Get the cached data for a project by its unique name.
    *
    * @param projectName name of the project.
-   * @return the cached data; null if no such project exists or a error occured.
+   * @return the cached data; null if no such project exists or a error occurred.
    * @see #checkedGet(com.google.gerrit.reviewdb.client.Project.NameKey)
    */
   public ProjectState get(Project.NameKey projectName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index ca15287..27e7b1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -115,11 +115,16 @@
   public ConfigInfo apply(ProjectResource rsrc, Input input)
       throws ResourceNotFoundException, BadRequestException,
       ResourceConflictException {
-    Project.NameKey projectName = rsrc.getNameKey();
     if (!rsrc.getControl().isOwner()) {
-      throw new ResourceNotFoundException(projectName.get());
+      throw new ResourceNotFoundException(rsrc.getName());
     }
+    return apply(rsrc.getControl(), input);
+  }
 
+  public ConfigInfo apply(ProjectControl ctrl, Input input)
+      throws ResourceNotFoundException, BadRequestException,
+      ResourceConflictException {
+    Project.NameKey projectName = ctrl.getProject().getNameKey();
     if (input == null) {
       throw new BadRequestException("config is required");
     }
@@ -169,7 +174,7 @@
       }
 
       if (input.pluginConfigValues != null) {
-        setPluginConfigValues(rsrc.getControl().getProjectState(),
+        setPluginConfigValues(ctrl.getProjectState(),
             projectConfig, input.pluginConfigValues);
       }
 
@@ -191,6 +196,8 @@
           throw new ResourceConflictException("Cannot update " + projectName
               + ": " + e.getCause().getMessage());
         } else {
+          log.warn(String.format("Failed to update config of project %s.",
+              projectName), e);
           throw new ResourceConflictException("Cannot update " + projectName);
         }
       }
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 69362d5..e8e29c1 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
@@ -117,7 +117,7 @@
 
   /**
    * @param ps patch set of the change to evaluate. If not set, the current
-   * patch set will be loaded from {@link #canSubmit()} or {@link
+   * patch set will be loaded from {@link #evaluate()} or {@link
    * #getSubmitType}.
    * @return this
    */
@@ -139,7 +139,7 @@
   }
 
   /**
-   * @param allow whether to allow {@link #canSubmit()} on closed changes.
+   * @param allow whether to allow {@link #evaluate()} on closed changes.
    * @return this
    */
   public SubmitRuleEvaluator setAllowClosed(boolean allow) {
@@ -148,7 +148,7 @@
   }
 
   /**
-   * @param allow whether to allow {@link #canSubmit()} on closed changes.
+   * @param allow whether to allow {@link #evaluate()} on draft changes.
    * @return this
    */
   public SubmitRuleEvaluator setAllowDraft(boolean allow) {
@@ -194,22 +194,26 @@
    * @return List of {@link SubmitRecord} objects returned from the evaluated
    *     rules, including any errors.
    */
-  public List<SubmitRecord> canSubmit() {
-    try {
-      initPatchSet();
-    } catch (OrmException e) {
-      return ruleError("Error looking up patch set "
-          + control.getChange().currentPatchSetId());
-    }
+  public List<SubmitRecord> evaluate() {
     Change c = control.getChange();
     if (!allowClosed && c.getStatus().isClosed()) {
       SubmitRecord rec = new SubmitRecord();
       rec.status = SubmitRecord.Status.CLOSED;
       return Collections.singletonList(rec);
     }
-    if ((c.getStatus() == Change.Status.DRAFT || patchSet.isDraft())
-        && !allowDraft) {
-      return cannotSubmitDraft();
+    if (!allowDraft) {
+      if (c.getStatus() == Change.Status.DRAFT) {
+        return cannotSubmitDraft();
+      }
+      try {
+        initPatchSet();
+      } catch (OrmException e) {
+        return ruleError("Error looking up patch set "
+            + control.getChange().currentPatchSetId());
+      }
+      if (patchSet.isDraft()) {
+        return cannotSubmitDraft();
+      }
     }
 
     List<Term> results;
@@ -501,8 +505,6 @@
 
   private PrologEnvironment getPrologEnvironment(CurrentUser user)
       throws RuleEvalException {
-    checkState(patchSet != null,
-        "getPrologEnvironment() called before initPatchSet()");
     ProjectState projectState = control.getProjectControl().getProjectState();
     PrologEnvironment env;
     try {
@@ -526,7 +528,6 @@
     }
     env.set(StoredValues.REVIEW_DB, cd.db());
     env.set(StoredValues.CHANGE_DATA, cd);
-    env.set(StoredValues.PATCH_SET, patchSet);
     env.set(StoredValues.CHANGE_CONTROL, control);
     if (user != null) {
       env.set(StoredValues.CURRENT_USER, user);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index bd1fa0c..b3c6aeb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -370,9 +370,6 @@
       } catch (IllegalAccessException e) {
         throw error("Error in operator " + name + ":" + value, e);
       } catch (InvocationTargetException e) {
-        if (e.getCause() instanceof QueryParseException) {
-          throw (QueryParseException) e.getCause();
-        }
         throw error("Error in operator " + name + ":" + value, e.getCause());
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
index 1053d92..d167860 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BasicChangeRewrites.java
@@ -29,7 +29,7 @@
           new InvalidProvider<InternalChangeQuery>(),
           new InvalidProvider<ChangeQueryRewriter>(),
           null, null, null, null, null, null, null, null, null, null, null,
-          null, null, null, null, null, null, null, null));
+          null, null, null, null, null, null, null, null, null));
 
   private static final QueryRewriter.Definition<ChangeData, BasicChangeRewrites> mydef =
       new QueryRewriter.Definition<>(BasicChangeRewrites.class, BUILDER);
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 802cf65..d94921e 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
@@ -119,7 +119,7 @@
   public static void ensureAllPatchSetsLoaded(Iterable<ChangeData> changes)
       throws OrmException {
     for (ChangeData cd : changes) {
-      cd.patches();
+      cd.patchSets();
     }
   }
 
@@ -127,7 +127,7 @@
       throws OrmException {
     Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
     for (ChangeData cd : changes) {
-      if (cd.currentPatchSet == null && cd.patches == null) {
+      if (cd.currentPatchSet == null && cd.patchSets == null) {
         missing.put(cd.change().currentPatchSetId(), cd);
       }
     }
@@ -204,7 +204,7 @@
   private String commitMessage;
   private List<FooterLine> commitFooters;
   private PatchSet currentPatchSet;
-  private Collection<PatchSet> patches;
+  private Collection<PatchSet> patchSets;
   private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
   private List<PatchSetApproval> currentApprovals;
   private Map<Integer, List<String>> files = new HashMap<>();
@@ -450,6 +450,10 @@
     return change;
   }
 
+  public void setChange(Change c) {
+    change = c;
+  }
+
   public Change reloadChange() throws OrmException {
     change = db.changes().get(legacyId);
     return change;
@@ -468,7 +472,7 @@
       if (c == null) {
         return null;
       }
-      for (PatchSet p : patches()) {
+      for (PatchSet p : patchSets()) {
         if (p.getId().equals(c.currentPatchSetId())) {
           currentPatchSet = p;
           return p;
@@ -536,23 +540,28 @@
    * @return patches for the change.
    * @throws OrmException an error occurred reading the database.
    */
-  public Collection<PatchSet> patches()
+  public Collection<PatchSet> patchSets()
       throws OrmException {
-    if (patches == null) {
-      patches = db.patchSets().byChange(legacyId).toList();
+    if (patchSets == null) {
+      patchSets = db.patchSets().byChange(legacyId).toList();
     }
-    return patches;
+    return patchSets;
+  }
+
+  public void setPatchSets(Collection<PatchSet> patchSets) {
+    this.currentPatchSet = null;
+    this.patchSets = patchSets;
   }
 
   /**
-   * @return patch with the given ID, or null if it does not exist.
+   * @return patch set with the given ID, or null if it does not exist.
    * @throws OrmException an error occurred reading the database.
    */
-  public PatchSet patch(PatchSet.Id psId) throws OrmException {
+  public PatchSet patchSet(PatchSet.Id psId) throws OrmException {
     if (currentPatchSet != null && currentPatchSet.getId().equals(psId)) {
       return currentPatchSet;
     }
-    for (PatchSet ps : patches()) {
+    for (PatchSet ps : patchSets()) {
       if (ps.getId().equals(psId)) {
         return ps;
       }
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 5058f0c..a0a7a3c 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
@@ -34,8 +34,11 @@
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.account.VersionedAccountQueries;
 import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.AllUsersNameProvider;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -56,9 +59,13 @@
 import com.google.inject.ProvisionException;
 import com.google.inject.util.Providers;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
 
+import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -107,6 +114,7 @@
   public static final String FIELD_PATH = "path";
   public static final String FIELD_PROJECT = "project";
   public static final String FIELD_PROJECTS = "projects";
+  public static final String FIELD_QUERY = "query";
   public static final String FIELD_REF = "ref";
   public static final String FIELD_REVIEWER = "reviewer";
   public static final String FIELD_REVIEWERIN = "reviewerin";
@@ -138,6 +146,7 @@
     final AccountResolver accountResolver;
     final GroupBackend groupBackend;
     final AllProjectsName allProjectsName;
+    final AllUsersNameProvider allUsersName;
     final PatchListCache patchListCache;
     final GitRepositoryManager repoManager;
     final ProjectCache projectCache;
@@ -165,6 +174,7 @@
         AccountResolver accountResolver,
         GroupBackend groupBackend,
         AllProjectsName allProjectsName,
+        AllUsersNameProvider allUsersName,
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
         ProjectCache projectCache,
@@ -177,9 +187,9 @@
       this(db, queryProvider, rewriter, userFactory, self,
           capabilityControlFactory, changeControlGenericFactory,
           changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
-          allProjectsName, patchListCache, repoManager, projectCache,
-          listChildProjects, indexes, submitStrategyFactory, conflictsCache,
-          trackingFooters,
+          allProjectsName, allUsersName, patchListCache, repoManager,
+          projectCache, listChildProjects, indexes, submitStrategyFactory,
+          conflictsCache, trackingFooters,
           cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true));
     }
 
@@ -197,6 +207,7 @@
         AccountResolver accountResolver,
         GroupBackend groupBackend,
         AllProjectsName allProjectsName,
+        AllUsersNameProvider allUsersName,
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
         ProjectCache projectCache,
@@ -219,6 +230,7 @@
      this.accountResolver = accountResolver;
      this.groupBackend = groupBackend;
      this.allProjectsName = allProjectsName;
+     this.allUsersName = allUsersName;
      this.patchListCache = patchListCache;
      this.repoManager = repoManager;
      this.projectCache = projectCache;
@@ -235,9 +247,9 @@
           Providers.of(otherUser),
           capabilityControlFactory, changeControlGenericFactory,
           changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
-          allProjectsName, patchListCache, repoManager, projectCache,
-          listChildProjects, indexes, submitStrategyFactory, conflictsCache,
-          trackingFooters, allowsDrafts);
+          allProjectsName, allUsersName, patchListCache, repoManager,
+          projectCache, listChildProjects, indexes, submitStrategyFactory,
+          conflictsCache, trackingFooters, allowsDrafts);
     }
 
     Arguments asUser(Account.Id otherId) {
@@ -765,6 +777,25 @@
     return Predicate.or(owner(ownerIds), commentby(ownerIds));
   }
 
+  @Operator
+  public Predicate<ChangeData> query(String name) throws QueryParseException {
+    AllUsersName allUsers = args.allUsersName.get();
+    try (Repository git = args.repoManager.openRepository(allUsers)) {
+      VersionedAccountQueries q = VersionedAccountQueries.forUser(self());
+      q.load(git);
+      String query = q.getQueryList().getQuery(name);
+      if (query != null) {
+        return parse(query);
+      }
+    } catch (RepositoryNotFoundException e) {
+      throw new QueryParseException("Unknown named query (no " +
+          allUsers.get() +" repo): " + name, e);
+    } catch (IOException | ConfigInvalidException e) {
+      throw new QueryParseException("Error parsing named query: " + name, e);
+    }
+    throw new QueryParseException("Unknown named query: " + name);
+  }
+
   @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/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index 14daa4d..3dd7c61 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -32,7 +32,7 @@
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
-    for (PatchSet p : object.patches()) {
+    for (PatchSet p : object.patchSets()) {
       if (p.getRevision() != null && p.getRevision().get() != null) {
         final ObjectId id = ObjectId.fromString(p.getRevision().get());
         if (abbrevId.prefixCompare(id) == 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 52846ed..f73e0e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -200,7 +200,7 @@
             eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
                 .setAllowClosed(true)
                 .setAllowDraft(true)
-                .canSubmit());
+                .evaluate());
           }
 
           if (includeCommitMessage) {
@@ -209,11 +209,11 @@
 
           if (includePatchSets) {
             if (includeFiles) {
-              eventFactory.addPatchSets(c, d.patches(),
+              eventFactory.addPatchSets(c, d.patchSets(),
                 includeApprovals ? d.approvals().asMap() : null,
                 includeFiles, d.change(), labelTypes);
             } else {
-              eventFactory.addPatchSets(c, d.patches(),
+              eventFactory.addPatchSets(c, d.patchSets(),
                   includeApprovals ? d.approvals().asMap() : null,
                   labelTypes);
             }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
index 63f3201..195a3e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.util;
 
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
@@ -26,8 +27,7 @@
 
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Set;
 
 /**
  * It parses from a configuration file submodule sections.
@@ -69,8 +69,8 @@
     this.superProjectBranch = superProjectBranch;
   }
 
-  public List<SubmoduleSubscription> parseAllSections() {
-    List<SubmoduleSubscription> parsedSubscriptions = new ArrayList<>();
+  public Set<SubmoduleSubscription> parseAllSections() {
+    Set<SubmoduleSubscription> parsedSubscriptions = Sets.newHashSet();
     for (final String id : bbc.getSubsections("submodule")) {
       final SubmoduleSubscription subscription = parse(id);
       if (subscription != null) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
index 0d81ee2..a437477 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
@@ -14,13 +14,12 @@
 
 package com.google.gerrit.server.change;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.inject.Scopes.SINGLETON;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.replay;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
 import com.google.common.base.MoreObjects;
@@ -33,7 +32,6 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
-import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
@@ -94,7 +92,6 @@
 import org.junit.runner.RunWith;
 
 import java.sql.Timestamp;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -117,13 +114,16 @@
   private Injector injector;
   private ReviewDb db;
   private Project.NameKey project;
+  private Account.Id ownerId;
   private RevisionResource revRes1;
   private RevisionResource revRes2;
+  private RevisionResource revRes3;
   private PatchLineComment plc1;
   private PatchLineComment plc2;
   private PatchLineComment plc3;
   private PatchLineComment plc4;
   private PatchLineComment plc5;
+  private PatchLineComment plc6;
   private IdentifiedUser changeOwner;
 
   @Inject private AllUsersNameProvider allUsers;
@@ -160,7 +160,7 @@
     co.setFullName("Change Owner");
     co.setPreferredEmail("change@owner.com");
     accountCache.put(co);
-    final Account.Id ownerId = co.getId();
+    ownerId = co.getId();
 
     Account ou = new Account(new Account.Id(2), TimeUtil.nowTs());
     ou.setFullName("Other Account");
@@ -227,48 +227,60 @@
     PatchLineCommentAccess plca = createMock(PatchLineCommentAccess.class);
     expect(db.patchComments()).andReturn(plca).anyTimes();
 
-    Change change = newChange();
-    PatchSet.Id psId1 = new PatchSet.Id(change.getId(), 1);
+    Change change1 = newChange();
+    PatchSet.Id psId1 = new PatchSet.Id(change1.getId(), 1);
     PatchSet ps1 = new PatchSet(psId1);
-    PatchSet.Id psId2 = new PatchSet.Id(change.getId(), 2);
+    PatchSet.Id psId2 = new PatchSet.Id(change1.getId(), 2);
     PatchSet ps2 = new PatchSet(psId2);
 
+    Change change2 = newChange();
+    PatchSet.Id psId3 = new PatchSet.Id(change2.getId(), 1);
+    PatchSet ps3 = new PatchSet(psId3);
+
     long timeBase = TimeUtil.roundToSecond(TimeUtil.nowTs()).getTime();
     plc1 = newPatchLineComment(psId1, "Comment1", null,
         "FileOne.txt", Side.REVISION, 3, ownerId, timeBase,
         "First Comment", new CommentRange(1, 2, 3, 4));
-    plc1.setRevId(new RevId("ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD"));
+    plc1.setRevId(new RevId("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"));
     plc2 = newPatchLineComment(psId1, "Comment2", "Comment1",
         "FileOne.txt", Side.REVISION, 3, otherUserId, timeBase + 1000,
         "Reply to First Comment",  new CommentRange(1, 2, 3, 4));
-    plc2.setRevId(new RevId("ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD"));
+    plc2.setRevId(new RevId("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"));
     plc3 = newPatchLineComment(psId1, "Comment3", "Comment1",
         "FileOne.txt", Side.PARENT, 3, ownerId, timeBase + 2000,
         "First Parent Comment",  new CommentRange(1, 2, 3, 4));
-    plc3.setRevId(new RevId("CDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEFCDEF"));
+    plc3.setRevId(new RevId("cdefcdefcdefcdefcdefcdefcdefcdefcdefcdef"));
     plc4 = newPatchLineComment(psId2, "Comment4", null, "FileOne.txt",
         Side.REVISION, 3, ownerId, timeBase + 3000, "Second Comment",
         new CommentRange(1, 2, 3, 4), Status.DRAFT);
-    plc4.setRevId(new RevId("BCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDE"));
+    plc4.setRevId(new RevId("bcdebcdebcdebcdebcdebcdebcdebcdebcdebcde"));
     plc5 = newPatchLineComment(psId2, "Comment5", null, "FileOne.txt",
         Side.REVISION, 5, ownerId, timeBase + 4000, "Third Comment",
         new CommentRange(3, 4, 5, 6), Status.DRAFT);
-    plc5.setRevId(new RevId("BCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDEBCDE"));
+    plc5.setRevId(new RevId("bcdebcdebcdebcdebcdebcdebcdebcdebcdebcde"));
+    plc6 = newPatchLineComment(psId3, "Comment6", null, "FileOne.txt",
+        Side.REVISION, 5, ownerId, timeBase + 5000, "Sixth Comment",
+        new CommentRange(3, 4, 5, 6), Status.DRAFT);
+    plc6.setRevId(new RevId("1234123412341234123412341234123412341234"));
 
     List<PatchLineComment> commentsByOwner = Lists.newArrayList();
     commentsByOwner.add(plc1);
     commentsByOwner.add(plc3);
     List<PatchLineComment> commentsByReviewer = Lists.newArrayList();
     commentsByReviewer.add(plc2);
-    List<PatchLineComment> drafts = Lists.newArrayList();
-    drafts.add(plc4);
-    drafts.add(plc5);
+    List<PatchLineComment> drafts1 = Lists.newArrayList();
+    drafts1.add(plc4);
+    drafts1.add(plc5);
+    List<PatchLineComment> drafts2 = Lists.newArrayList();
+    drafts2.add(plc6);
 
     plca.upsert(commentsByOwner);
     expectLastCall().anyTimes();
     plca.upsert(commentsByReviewer);
     expectLastCall().anyTimes();
-    plca.upsert(drafts);
+    plca.upsert(drafts1);
+    expectLastCall().anyTimes();
+    plca.upsert(drafts2);
     expectLastCall().anyTimes();
 
     expect(plca.publishedByPatchSet(psId1))
@@ -279,28 +291,36 @@
         .andAnswer(results()).anyTimes();
     expect(plca.draftByPatchSetAuthor(psId2, ownerId))
         .andAnswer(results(plc4, plc5)).anyTimes();
-    expect(plca.byChange(change.getId()))
+    expect(plca.byChange(change1.getId()))
         .andAnswer(results(plc1, plc2, plc3, plc4, plc5)).anyTimes();
+    expect(plca.draftByAuthor(ownerId))
+        .andAnswer(results(plc4, plc5, plc6)).anyTimes();
     replay(db, plca);
 
-    ChangeUpdate update = newUpdate(change, changeOwner);
+    ChangeUpdate update = newUpdate(change1, changeOwner);
     update.setPatchSetId(psId1);
     plcUtil.upsertComments(db, update, commentsByOwner);
     update.commit();
 
-    update = newUpdate(change, otherUser);
+    update = newUpdate(change1, otherUser);
     update.setPatchSetId(psId1);
     plcUtil.upsertComments(db, update, commentsByReviewer);
     update.commit();
 
-    update = newUpdate(change, changeOwner);
+    update = newUpdate(change1, changeOwner);
     update.setPatchSetId(psId2);
-    plcUtil.upsertComments(db, update, drafts);
+    plcUtil.upsertComments(db, update, drafts1);
     update.commit();
 
-    ChangeControl ctl = stubChangeControl(change);
+    update = newUpdate(change2, changeOwner);
+    update.setPatchSetId(psId3);
+    plcUtil.upsertComments(db, update, drafts2);
+    update.commit();
+
+    ChangeControl ctl = stubChangeControl(change1);
     revRes1 = new RevisionResource(new ChangeResource(ctl, null), ps1);
     revRes2 = new RevisionResource(new ChangeResource(ctl, null), ps2);
+    revRes3 = new RevisionResource(new ChangeResource(stubChangeControl(change2), null), ps3);
   }
 
   private ChangeControl stubChangeControl(Change c) throws OrmException {
@@ -325,7 +345,7 @@
 
     // test ListComments for patch set 2
     assertListComments(revRes2,
-        Collections.<String, ArrayList<PatchLineComment>>emptyMap());
+        Collections.<String, List<PatchLineComment>>emptyMap());
   }
 
   @Test
@@ -341,7 +361,7 @@
   public void testListDrafts() throws Exception {
     // test ListDrafts for patch set 1
     assertListDrafts(revRes1,
-        Collections.<String, ArrayList<PatchLineComment>> emptyMap());
+        Collections.<String, List<PatchLineComment>> emptyMap());
 
     // test ListDrafts for patch set 2
     assertListDrafts(revRes2, ImmutableMap.of(
@@ -350,18 +370,18 @@
 
   @Test
   public void testPatchLineCommentsUtilByCommentStatus() throws OrmException {
-    List<PatchLineComment> publishedActual =
-        plcUtil.publishedByChange(db, revRes2.getNotes());
-    List<PatchLineComment> draftActual =
-        plcUtil.draftByChange(db, revRes2.getNotes());
-    List<PatchLineComment> publishedExpected =
-        Lists.newArrayList(plc1, plc2, plc3);
-    List<PatchLineComment> draftExpected =
-        Lists.newArrayList(plc4, plc5);
-    assertEquals(publishedExpected.size(), publishedActual.size());
-    assertEquals(draftExpected.size(), draftActual.size());
-    assertEquals(publishedExpected, publishedActual);
-    assertEquals(draftExpected, draftActual);
+    assertThat(plcUtil.publishedByChange(db, revRes2.getNotes()))
+        .containsExactly(plc3, plc1, plc2).inOrder();
+    assertThat(plcUtil.draftByChange(db, revRes2.getNotes()))
+        .containsExactly(plc4, plc5).inOrder();
+  }
+
+  @Test
+  public void testPatchLineCommentsUtilDraftByChangeAuthor() throws Exception {
+    assertThat(plcUtil.draftByChangeAuthor(db, revRes1.getNotes(), ownerId))
+        .containsExactly(plc4, plc5).inOrder();
+    assertThat(plcUtil.draftByChangeAuthor(db, revRes3.getNotes(), ownerId))
+        .containsExactly(plc6);
   }
 
   private static IAnswer<ResultSet<PatchLineComment>> results(
@@ -390,66 +410,51 @@
   }
 
   private void assertListComments(RevisionResource res,
-      Map<String, ArrayList<PatchLineComment>> expected) throws Exception {
-    RestReadView<RevisionResource> listView =
-        (RestReadView<RevisionResource>) comments.list();
-    @SuppressWarnings("unchecked")
-    Map<String, List<CommentInfo>> actual =
-        (Map<String, List<CommentInfo>>) listView.apply(res);
-    assertNotNull(actual);
-    assertEquals(expected.size(), actual.size());
-    assertEquals(expected.keySet(), actual.keySet());
-    for (Map.Entry<String, ArrayList<PatchLineComment>> entry : expected.entrySet()) {
-      List<PatchLineComment> expectedComments = entry.getValue();
-      List<CommentInfo> actualComments = actual.get(entry.getKey());
-      assertNotNull(actualComments);
-      assertEquals(expectedComments.size(), actualComments.size());
-      for (int i = 0; i < expectedComments.size(); i++) {
-        assertComment(expectedComments.get(i), actualComments.get(i), true);
-      }
-    }
+      Map<String, ? extends List<PatchLineComment>> expected) throws Exception {
+    assertCommentMap(comments.list().apply(res), expected, true);
   }
 
   private void assertListDrafts(RevisionResource res,
-      Map<String, ArrayList<PatchLineComment>> expected) throws Exception {
-    RestReadView<RevisionResource> listView =
-        (RestReadView<RevisionResource>) drafts.list();
-    @SuppressWarnings("unchecked")
-    Map<String, List<CommentInfo>> actual =
-        (Map<String, List<CommentInfo>>) listView.apply(res);
-    assertNotNull(actual);
-    assertEquals(expected.size(), actual.size());
-    assertEquals(expected.keySet(), actual.keySet());
-    for (Map.Entry<String, ArrayList<PatchLineComment>> entry : expected.entrySet()) {
-      List<PatchLineComment> expectedComments = entry.getValue();
-      List<CommentInfo> actualComments = actual.get(entry.getKey());
-      assertNotNull(actualComments);
-      assertEquals(expectedComments.size(), actualComments.size());
-      for (int i = 0; i < expectedComments.size(); i++) {
-        assertComment(expectedComments.get(i), actualComments.get(i), false);
+      Map<String, ? extends List<PatchLineComment>> expected) throws Exception {
+    assertCommentMap(drafts.list().apply(res), expected, false);
+  }
+
+  private void assertCommentMap(Map<String, List<CommentInfo>> actual,
+      Map<String, ? extends List<PatchLineComment>> expected,
+      boolean isPublished) {
+    assertThat(actual.keySet()).containsExactlyElementsIn(expected.keySet());
+    for (Map.Entry<String, List<CommentInfo>> entry : actual.entrySet()) {
+      List<CommentInfo> actualList = entry.getValue();
+      List<PatchLineComment> expectedList = expected.get(entry.getKey());
+      assertThat(actualList).hasSize(expectedList.size());
+      for (int i = 0; i < expectedList.size(); i++) {
+        assertComment(expectedList.get(i), actualList.get(i), isPublished);
       }
     }
   }
 
   private static void assertComment(PatchLineComment plc, CommentInfo ci,
       boolean isPublished) {
-    assertEquals(plc.getKey().get(), ci.id);
-    assertEquals(plc.getParentUuid(), ci.inReplyTo);
-    assertEquals(plc.getMessage(), ci.message);
+    assertThat(ci.id).isEqualTo(plc.getKey().get());
+    assertThat(ci.inReplyTo).isEqualTo(plc.getParentUuid());
+    assertThat(ci.message).isEqualTo(plc.getMessage());
     if (isPublished) {
-      assertNotNull(ci.author);
-      assertEquals(plc.getAuthor(), new Account.Id(ci.author._accountId));
+      assertThat(ci.author).isNotNull();
+      assertThat(new Account.Id(ci.author._accountId))
+          .isEqualTo(plc.getAuthor());
     }
-    assertEquals(plc.getLine(), (int) ci.line);
-    assertEquals(plc.getSide() == 0 ? Side.PARENT : Side.REVISION,
-        MoreObjects.firstNonNull(ci.side, Side.REVISION));
-    assertEquals(TimeUtil.roundToSecond(plc.getWrittenOn()),
-        TimeUtil.roundToSecond(ci.updated));
-    assertEquals(plc.getWrittenOn(), ci.updated);
-    assertEquals(plc.getRange().getStartLine(), ci.range.startLine);
-    assertEquals(plc.getRange().getStartCharacter(), ci.range.startCharacter);
-    assertEquals(plc.getRange().getEndLine(), ci.range.endLine);
-    assertEquals(plc.getRange().getEndCharacter(), ci.range.endCharacter);
+    assertThat(ci.line).isEqualTo(plc.getLine());
+    assertThat(MoreObjects.firstNonNull(ci.side, Side.REVISION))
+        .isEqualTo(plc.getSide() == 0 ? Side.PARENT : Side.REVISION);
+    assertThat(TimeUtil.roundToSecond(ci.updated))
+        .isEqualTo(TimeUtil.roundToSecond(plc.getWrittenOn()));
+    assertThat(ci.updated).isEqualTo(plc.getWrittenOn());
+    assertThat(ci.range.startLine).isEqualTo(plc.getRange().getStartLine());
+    assertThat(ci.range.startCharacter)
+        .isEqualTo(plc.getRange().getStartCharacter());
+    assertThat(ci.range.endLine).isEqualTo(plc.getRange().getEndLine());
+    assertThat(ci.range.endCharacter)
+        .isEqualTo(plc.getRange().getEndCharacter());
   }
 
   private static PatchLineComment newPatchLineComment(PatchSet.Id psId,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
index 2ad813d..8caba88 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/ConsistencyCheckerTest.java
@@ -37,10 +37,10 @@
 import com.google.gerrit.testutil.FakeAccountByEmailCache;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
 import com.google.gerrit.testutil.TestChanges;
 import com.google.inject.util.Providers;
 
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -59,7 +59,7 @@
   private InMemoryRepositoryManager repoManager;
   private ConsistencyChecker checker;
 
-  private TestRepository<InMemoryRepository> repo;
+  private TestRepository<Repo> repo;
   private Project.NameKey project;
   private Account.Id userId;
   private RevCommit tip;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
index d5b722c..6100ffd 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
@@ -23,45 +23,45 @@
 public class HashtagsTest {
   @Test
   public void emptyCommitMessage() throws Exception {
-    assertThat((Iterable<?>)HashtagsUtil.extractTags("")).isEmpty();
+    assertThat(HashtagsUtil.extractTags("")).isEmpty();
   }
 
   @Test
   public void nullCommitMessage() throws Exception {
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(null)).isEmpty();
+    assertThat(HashtagsUtil.extractTags(null)).isEmpty();
   }
 
   @Test
   public void noHashtags() throws Exception {
     String commitMessage = "Subject\n\nLine 1\n\nLine 2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage)).isEmpty();
+    assertThat(HashtagsUtil.extractTags(commitMessage)).isEmpty();
   }
 
   @Test
   public void singleHashtag() throws Exception {
     String commitMessage = "#Subject\n\nLine 1\n\nLine 2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Subject"));
   }
 
   @Test
   public void singleHashtagNumeric() throws Exception {
     String commitMessage = "Subject\n\n#123\n\nLine 2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("123"));
   }
 
   @Test
   public void multipleHashtags() throws Exception {
     String commitMessage = "#Subject\n\n#Hashtag\n\nLine 2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Subject", "Hashtag"));
   }
 
   @Test
   public void repeatedHashtag() throws Exception {
     String commitMessage = "#Subject\n\n#Hashtag1\n\n#Hashtag2\n\n#Hashtag1";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(
           Sets.newHashSet("Subject", "Hashtag1", "Hashtag2"));
   }
@@ -69,21 +69,21 @@
   @Test
   public void multipleHashtagsNoSpaces() throws Exception {
     String commitMessage = "Subject\n\n#Hashtag1#Hashtag2";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Hashtag1"));
   }
 
   @Test
   public void hyphenatedHashtag() throws Exception {
     String commitMessage = "Subject\n\n#Hyphenated-Hashtag";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Hyphenated-Hashtag"));
   }
 
   @Test
   public void underscoredHashtag() throws Exception {
     String commitMessage = "Subject\n\n#Underscored_Hashtag";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(Sets.newHashSet("Underscored_Hashtag"));
   }
 
@@ -91,7 +91,7 @@
   public void hashtagsWithAccentedCharacters() throws Exception {
     String commitMessage = "Jag #måste #öva på min #Svenska!\n\n"
         + "Jag behöver en #läkare.";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage))
+    assertThat(HashtagsUtil.extractTags(commitMessage))
       .containsExactlyElementsIn(
           Sets.newHashSet("måste", "öva", "Svenska", "läkare"));
   }
@@ -99,6 +99,6 @@
   @Test
   public void hashWithoutHashtag() throws Exception {
     String commitMessage = "Subject\n\n# Text";
-    assertThat((Iterable<?>)HashtagsUtil.extractTags(commitMessage)).isEmpty();
+    assertThat(HashtagsUtil.extractTags(commitMessage)).isEmpty();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
index 2ebad52..ca154b1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/LocalDiskRepositoryManagerTest.java
@@ -73,7 +73,7 @@
     try (Repository repo = repoManager.openRepository(projectA)) {
       assertThat(repo).isNotNull();
     }
-    assertThat((Iterable<?>) repoManager.list()).containsExactly(projectA);
+    assertThat(repoManager.list()).containsExactly(projectA);
   }
 
   @Test(expected = RepositoryNotFoundException.class)
@@ -174,7 +174,7 @@
     try (Repository repo = repoManager.openRepository(projectA)) {
       assertThat(repo).isNotNull();
     }
-    assertThat((Iterable<?>) repoManager.list()).containsExactly(projectA);
+    assertThat(repoManager.list()).containsExactly(projectA);
   }
 
   @Test(expected = RepositoryNotFoundException.class)
@@ -196,8 +196,8 @@
     repoManager.getBasePath().resolve(".git").toFile().mkdir();
     // create an invalid repo name
     createRepository(repoManager.getBasePath(), "project?A");
-    assertThat((Iterable<?>) repoManager.list()).containsExactly(projectA,
-        projectB, projectC);
+    assertThat(repoManager.list())
+        .containsExactly(projectA, projectB, projectC);
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/QueryListTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/QueryListTest.java
new file mode 100644
index 0000000..d022d3e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/QueryListTest.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.replay;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class QueryListTest extends TestCase {
+  public static final String Q_P = "project:foo";
+  public static final String Q_B = "branch:bar";
+  public static final String Q_COMPLEX = "branch:bar AND peers:'is:open\t'";
+
+  public static final String N_FOO = "foo";
+  public static final String N_BAR = "bar";
+
+  public static final String L_FOO = N_FOO + "\t" + Q_P + "\n";
+  public static final String L_BAR = N_BAR + "\t" + Q_B + "\n";
+  public static final String L_FOO_PROP = N_FOO + "   \t" + Q_P + "\n";
+  public static final String L_BAR_PROP = N_BAR + "   \t" + Q_B + "\n";
+  public static final String L_FOO_PAD_F = " " + N_FOO + "\t" + Q_P + "\n";
+  public static final String L_FOO_PAD_E = N_FOO + " \t" + Q_P + "\n";
+  public static final String L_BAR_PAD_F = N_BAR + "\t " + Q_B + "\n";
+  public static final String L_BAR_PAD_E = N_BAR + "\t" + Q_B + " \n";
+  public static final String L_COMPLEX = N_FOO + "\t" + Q_COMPLEX + "\t \n";
+  public static final String L_BAD = N_FOO + "\n";
+
+  public static final String HEADER = "# Name\tQuery\n";
+  public static final String C1 = "# A Simple Comment\n";
+  public static final String C2 = "# Comment with a tab\t and multi # # #\n";
+
+  public static final String F_SIMPLE = L_FOO + L_BAR;
+  public static final String F_PROPER = L_BAR_PROP + L_FOO_PROP; // alpha order
+  public static final String F_PAD_F = L_FOO_PAD_F + L_BAR_PAD_F;
+  public static final String F_PAD_E = L_FOO_PAD_E + L_BAR_PAD_E;
+
+  @Test
+  public void testParseSimple() throws Exception {
+    QueryList ql = QueryList.parse(F_SIMPLE, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseWHeader() throws Exception {
+    QueryList ql = QueryList.parse(HEADER + F_SIMPLE, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseWComments() throws Exception {
+    QueryList ql = QueryList.parse(C1 + F_SIMPLE + C2, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseFooComment() throws Exception {
+    QueryList ql = QueryList.parse("#" + L_FOO + L_BAR, null);
+    assertThat(ql.getQuery(N_FOO)).isNull();
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParsePaddedFronts() throws Exception {
+    QueryList ql = QueryList.parse(F_PAD_F, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParsePaddedEnds() throws Exception {
+    QueryList ql = QueryList.parse(F_PAD_E, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
+    assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
+  }
+
+  @Test
+  public void testParseComplex() throws Exception {
+    QueryList ql = QueryList.parse(L_COMPLEX, null);
+    assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_COMPLEX);
+  }
+
+  @Test(expected = IOException.class)
+  public void testParseBad() throws Exception {
+    ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
+    replay(sink);
+    QueryList.parse(L_BAD, sink);
+  }
+
+  @Test
+  public void testAsText() throws Exception {
+    String expectedText = HEADER + "#\n" + F_PROPER;
+    QueryList ql = QueryList.parse(F_SIMPLE, null);
+    String asText = ql.asText();
+    assertThat(asText).isEqualTo(expectedText);
+
+    ql = QueryList.parse(asText, null);
+    asText = ql.asText();
+    assertThat(asText).isEqualTo(expectedText);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
index c9a2056..0c8625d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
@@ -27,7 +27,7 @@
           FakeQueryBuilder.class),
         new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
           null, null, null, null, null, null, null, null, null, null, null,
-          indexes, null, null, null, null));
+          null, indexes, null, null, null, null));
   }
 
   @Operator
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 1308723..108c20f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -203,16 +203,16 @@
     return label;
   }
 
-  protected PatchLineComment newPublishedPatchLineComment(PatchSet.Id psId,
+  protected PatchLineComment newPublishedComment(PatchSet.Id psId,
       String filename, String UUID, CommentRange range, int line,
       IdentifiedUser commenter, String parentUUID, Timestamp t,
       String message, short side, String commitSHA1) {
-    return newPatchLineComment(psId, filename, UUID, range, line, commenter,
+    return newComment(psId, filename, UUID, range, line, commenter,
         parentUUID, t, message, side, commitSHA1,
         PatchLineComment.Status.PUBLISHED);
   }
 
-  protected PatchLineComment newPatchLineComment(PatchSet.Id psId,
+  protected PatchLineComment newComment(PatchSet.Id psId,
       String filename, String UUID, CommentRange range, int line,
       IdentifiedUser commenter, String parentUUID, Timestamp t,
       String message, short side, String commitSHA1,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index aea966a..6067442 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -14,23 +14,19 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.notedb.ReviewerState.CC;
 import static com.google.gerrit.server.notedb.ReviewerState.REVIEWER;
 import static com.google.gerrit.testutil.TestChanges.incrementPatchSet;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.collect.Iterables;
-import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Multimap;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -41,8 +37,8 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
-import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -50,12 +46,12 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.junit.Test;
 
-import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
@@ -71,22 +67,23 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(1, notes.getApprovals().keySet().size());
+    assertThat(notes.getApprovals().keySet())
+        .containsExactly(c.currentPatchSetId());
     List<PatchSetApproval> psas =
       notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(2, psas.size());
+    assertThat(psas).hasSize(2);
 
-    assertEquals(c.currentPatchSetId(), psas.get(0).getPatchSetId());
-    assertEquals(1, psas.get(0).getAccountId().get());
-    assertEquals("Code-Review", psas.get(0).getLabel());
-    assertEquals((short) -1, psas.get(0).getValue());
-    assertEquals(truncate(after(c, 1000)), psas.get(0).getGranted());
+    assertThat(psas.get(0).getPatchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(0).getAccountId().get()).isEqualTo(1);
+    assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
+    assertThat(psas.get(0).getValue()).isEqualTo((short) -1);
+    assertThat(psas.get(0).getGranted()).isEqualTo(truncate(after(c, 1000)));
 
-    assertEquals(c.currentPatchSetId(), psas.get(1).getPatchSetId());
-    assertEquals(1, psas.get(1).getAccountId().get());
-    assertEquals("Verified", psas.get(1).getLabel());
-    assertEquals((short) 1, psas.get(1).getValue());
-    assertEquals(psas.get(0).getGranted(), psas.get(1).getGranted());
+    assertThat(psas.get(1).getPatchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(1).getAccountId().get()).isEqualTo(1);
+    assertThat(psas.get(1).getLabel()).isEqualTo("Verified");
+    assertThat(psas.get(1).getValue()).isEqualTo((short) 1);
+    assertThat(psas.get(1).getGranted()).isEqualTo(psas.get(0).getGranted());
   }
 
   @Test
@@ -105,21 +102,21 @@
 
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, PatchSetApproval> psas = notes.getApprovals();
-    assertEquals(2, notes.getApprovals().keySet().size());
+    assertThat(psas).hasSize(2);
 
     PatchSetApproval psa1 = Iterables.getOnlyElement(psas.get(ps1));
-    assertEquals(ps1, psa1.getPatchSetId());
-    assertEquals(1, psa1.getAccountId().get());
-    assertEquals("Code-Review", psa1.getLabel());
-    assertEquals((short) -1, psa1.getValue());
-    assertEquals(truncate(after(c, 1000)), psa1.getGranted());
+    assertThat(psa1.getPatchSetId()).isEqualTo(ps1);
+    assertThat(psa1.getAccountId().get()).isEqualTo(1);
+    assertThat(psa1.getLabel()).isEqualTo("Code-Review");
+    assertThat(psa1.getValue()).isEqualTo((short) -1);
+    assertThat(psa1.getGranted()).isEqualTo(truncate(after(c, 1000)));
 
     PatchSetApproval psa2 = Iterables.getOnlyElement(psas.get(ps2));
-    assertEquals(ps2, psa2.getPatchSetId());
-    assertEquals(1, psa2.getAccountId().get());
-    assertEquals("Code-Review", psa2.getLabel());
-    assertEquals((short) +1, psa2.getValue());
-    assertEquals(truncate(after(c, 2000)), psa2.getGranted());
+    assertThat(psa2.getPatchSetId()).isEqualTo(ps2);
+    assertThat(psa2.getAccountId().get()).isEqualTo(1);
+    assertThat(psa2.getLabel()).isEqualTo("Code-Review");
+    assertThat(psa2.getValue()).isEqualTo((short) +1);
+    assertThat(psa2.getGranted()).isEqualTo(truncate(after(c, 2000)));
   }
 
   @Test
@@ -132,8 +129,8 @@
     ChangeNotes notes = newNotes(c);
     PatchSetApproval psa = Iterables.getOnlyElement(
         notes.getApprovals().get(c.currentPatchSetId()));
-    assertEquals("Code-Review", psa.getLabel());
-    assertEquals((short) -1, psa.getValue());
+    assertThat(psa.getLabel()).isEqualTo("Code-Review");
+    assertThat(psa.getValue()).isEqualTo((short) -1);
 
     update = newUpdate(c, changeOwner);
     update.putApproval("Code-Review", (short) 1);
@@ -142,8 +139,8 @@
     notes = newNotes(c);
     psa = Iterables.getOnlyElement(
         notes.getApprovals().get(c.currentPatchSetId()));
-    assertEquals("Code-Review", psa.getLabel());
-    assertEquals((short) 1, psa.getValue());
+    assertThat(psa.getLabel()).isEqualTo("Code-Review");
+    assertThat(psa.getValue()).isEqualTo((short) 1);
   }
 
   @Test
@@ -158,22 +155,23 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(1, notes.getApprovals().keySet().size());
+    assertThat(notes.getApprovals().keySet())
+        .containsExactly(c.currentPatchSetId());
     List<PatchSetApproval> psas =
       notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(2, psas.size());
+    assertThat(psas).hasSize(2);
 
-    assertEquals(c.currentPatchSetId(), psas.get(0).getPatchSetId());
-    assertEquals(1, psas.get(0).getAccountId().get());
-    assertEquals("Code-Review", psas.get(0).getLabel());
-    assertEquals((short) -1, psas.get(0).getValue());
-    assertEquals(truncate(after(c, 1000)), psas.get(0).getGranted());
+    assertThat(psas.get(0).getPatchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(0).getAccountId().get()).isEqualTo(1);
+    assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
+    assertThat(psas.get(0).getValue()).isEqualTo((short) -1);
+    assertThat(psas.get(0).getGranted()).isEqualTo(truncate(after(c, 1000)));
 
-    assertEquals(c.currentPatchSetId(), psas.get(1).getPatchSetId());
-    assertEquals(2, psas.get(1).getAccountId().get());
-    assertEquals("Code-Review", psas.get(1).getLabel());
-    assertEquals((short) 1, psas.get(1).getValue());
-    assertEquals(truncate(after(c, 2000)), psas.get(1).getGranted());
+    assertThat(psas.get(1).getPatchSetId()).isEqualTo(c.currentPatchSetId());
+    assertThat(psas.get(1).getAccountId().get()).isEqualTo(2);
+    assertThat(psas.get(1).getLabel()).isEqualTo("Code-Review");
+    assertThat(psas.get(1).getValue()).isEqualTo((short) 1);
+    assertThat(psas.get(1).getGranted()).isEqualTo(truncate(after(c, 2000)));
   }
 
   @Test
@@ -186,16 +184,16 @@
     ChangeNotes notes = newNotes(c);
     PatchSetApproval psa = Iterables.getOnlyElement(
         notes.getApprovals().get(c.currentPatchSetId()));
-    assertEquals(1, psa.getAccountId().get());
-    assertEquals("Not-For-Long", psa.getLabel());
-    assertEquals((short) 1, psa.getValue());
+    assertThat(psa.getAccountId().get()).isEqualTo(1);
+    assertThat(psa.getLabel()).isEqualTo("Not-For-Long");
+    assertThat(psa.getValue()).isEqualTo((short) 1);
 
     update = newUpdate(c, changeOwner);
     update.removeApproval("Not-For-Long");
     update.commit();
 
     notes = newNotes(c);
-    assertTrue(notes.getApprovals().isEmpty());
+    assertThat(notes.getApprovals()).isEmpty();
   }
 
   @Test
@@ -207,10 +205,10 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(ImmutableSetMultimap.of(
+    assertThat(notes.getReviewers()).isEqualTo(
+        ImmutableSetMultimap.of(
           REVIEWER, new Account.Id(1),
-          REVIEWER, new Account.Id(2)),
-        notes.getReviewers());
+          REVIEWER, new Account.Id(2)));
   }
 
   @Test
@@ -222,10 +220,10 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(ImmutableSetMultimap.of(
-          REVIEWER, new Account.Id(1),
-          CC, new Account.Id(2)),
-        notes.getReviewers());
+    assertThat(notes.getReviewers()).isEqualTo(
+        ImmutableSetMultimap.of(
+            REVIEWER, new Account.Id(1),
+            CC, new Account.Id(2)));
   }
 
   @Test
@@ -236,18 +234,16 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(ImmutableSetMultimap.of(
-          REVIEWER, new Account.Id(2)),
-        notes.getReviewers());
+    assertThat(notes.getReviewers()).isEqualTo(
+        ImmutableSetMultimap.of(REVIEWER, new Account.Id(2)));
 
     update = newUpdate(c, otherUser);
     update.putReviewer(otherUser.getAccount().getId(), CC);
     update.commit();
 
     notes = newNotes(c);
-    assertEquals(ImmutableSetMultimap.of(
-          CC, new Account.Id(2)),
-        notes.getReviewers());
+    assertThat(notes.getReviewers()).isEqualTo(
+        ImmutableSetMultimap.of(CC, new Account.Id(2)));
   }
 
   @Test
@@ -268,9 +264,11 @@
     ChangeNotes notes = newNotes(c);
     List<PatchSetApproval> psas =
         notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(2, psas.size());
-    assertEquals(changeOwner.getAccount().getId(), psas.get(0).getAccountId());
-    assertEquals(otherUser.getAccount().getId(), psas.get(1).getAccountId());
+    assertThat(psas).hasSize(2);
+    assertThat(psas.get(0).getAccountId())
+        .isEqualTo(changeOwner.getAccount().getId());
+    assertThat(psas.get(1).getAccountId())
+        .isEqualTo(otherUser.getAccount().getId());
 
     update = newUpdate(c, changeOwner);
     update.removeReviewer(otherUser.getAccount().getId());
@@ -278,8 +276,9 @@
 
     notes = newNotes(c);
     psas = notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(1, psas.size());
-    assertEquals(changeOwner.getAccount().getId(), psas.get(0).getAccountId());
+    assertThat(psas).hasSize(1);
+    assertThat(psas.get(0).getAccountId())
+        .isEqualTo(changeOwner.getAccount().getId());
   }
 
   @Test
@@ -299,13 +298,15 @@
 
     ChangeNotes notes = newNotes(c);
     List<SubmitRecord> recs = notes.getSubmitRecords();
-    assertEquals(2, recs.size());
-    assertEquals(submitRecord("NOT_READY", null,
-        submitLabel("Verified", "OK", changeOwner.getAccountId()),
-        submitLabel("Code-Review", "NEED", null)), recs.get(0));
-    assertEquals(submitRecord("NOT_READY", null,
-        submitLabel("Verified", "OK", changeOwner.getAccountId()),
-        submitLabel("Alternative-Code-Review", "NEED", null)), recs.get(1));
+    assertThat(recs).hasSize(2);
+    assertThat(recs.get(0)).isEqualTo(
+        submitRecord("NOT_READY", null,
+          submitLabel("Verified", "OK", changeOwner.getAccountId()),
+          submitLabel("Code-Review", "NEED", null)));
+    assertThat(recs.get(1)).isEqualTo(
+        submitRecord("NOT_READY", null,
+          submitLabel("Verified", "OK", changeOwner.getAccountId()),
+          submitLabel("Alternative-Code-Review", "NEED", null)));
   }
 
   @Test
@@ -327,16 +328,16 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(submitRecord("OK", null,
-          submitLabel("Code-Review", "OK", changeOwner.getAccountId())),
-        Iterables.getOnlyElement(notes.getSubmitRecords()));
+    assertThat(notes.getSubmitRecords()).containsExactly(
+        submitRecord("OK", null,
+          submitLabel("Code-Review", "OK", changeOwner.getAccountId())));
   }
 
   @Test
   public void emptyChangeUpdate() throws Exception {
     ChangeUpdate update = newUpdate(newChange(), changeOwner);
     update.commit();
-    assertNull(update.getRevision());
+    assertThat(update.getRevision()).isNull();
   }
 
   @Test
@@ -351,7 +352,7 @@
     try (RevWalk walk = new RevWalk(repo)) {
       RevCommit commit = walk.parseCommit(update.getRevision());
       walk.parseBody(commit);
-      assertTrue(commit.getFullMessage().endsWith("Hashtags: tag1,tag2\n"));
+      assertThat(commit.getFullMessage()).endsWith("Hashtags: tag1,tag2\n");
     }
   }
 
@@ -366,7 +367,7 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(hashtags, notes.getHashtags());
+    assertThat(notes.getHashtags()).isEqualTo(hashtags);
   }
 
   @Test
@@ -374,7 +375,7 @@
     ChangeUpdate update = newUpdate(newChange(), changeOwner);
     update.setSubject("Create change");
     update.commit();
-    assertNotNull(update.getRevision());
+    assertThat(update.getRevision()).isNotNull();
   }
 
   @Test
@@ -398,15 +399,17 @@
     ChangeNotes notes = newNotes(c);
     List<PatchSetApproval> psas =
         notes.getApprovals().get(c.currentPatchSetId());
-    assertEquals(2, psas.size());
+    assertThat(psas).hasSize(2);
 
-    assertEquals(changeOwner.getAccount().getId(), psas.get(0).getAccountId());
-    assertEquals("Verified", psas.get(0).getLabel());
-    assertEquals((short) 1, psas.get(0).getValue());
+    assertThat(psas.get(0).getAccountId())
+        .isEqualTo(changeOwner.getAccount().getId());
+    assertThat(psas.get(0).getLabel()).isEqualTo("Verified");
+    assertThat(psas.get(0).getValue()).isEqualTo((short) 1);
 
-    assertEquals(otherUser.getAccount().getId(), psas.get(1).getAccountId());
-    assertEquals("Code-Review", psas.get(1).getLabel());
-    assertEquals((short) 2, psas.get(1).getValue());
+    assertThat(psas.get(1).getAccountId())
+        .isEqualTo(otherUser.getAccount().getId());
+    assertThat(psas.get(1).getLabel()).isEqualTo("Code-Review");
+    assertThat(psas.get(1).getValue()).isEqualTo((short) 2);
   }
 
   @Test
@@ -420,7 +423,7 @@
     PatchSet.Id psId = c.currentPatchSetId();
     BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
     BatchMetaDataUpdate batch = update1.openUpdateInBatch(bru);
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment1 = newPublishedComment(psId, "file1",
         uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
         (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update1.setPatchSetId(psId);
@@ -437,17 +440,17 @@
       ChangeNotes notes = newNotes(c);
       ObjectId tip = notes.getRevision();
       RevCommit commitWithApprovals = rw.parseCommit(tip);
-      assertNotNull(commitWithApprovals);
+      assertThat(commitWithApprovals).isNotNull();
       RevCommit commitWithComments = commitWithApprovals.getParent(0);
-      assertNotNull(commitWithComments);
+      assertThat(commitWithComments).isNotNull();
 
       ChangeNotesParser notesWithComments =
           new ChangeNotesParser(c, commitWithComments.copy(), rw, repoManager);
       notesWithComments.parseAll();
       ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals1 =
           notesWithComments.buildApprovals();
-      assertEquals(0, approvals1.size());
-      assertEquals(1, notesWithComments.commentsForBase.size());
+      assertThat(approvals1).isEmpty();
+      assertThat(notesWithComments.comments).hasSize(1);
       notesWithComments.close();
 
       ChangeNotesParser notesWithApprovals =
@@ -455,8 +458,8 @@
       notesWithApprovals.parseAll();
       ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals2 =
           notesWithApprovals.buildApprovals();
-      assertEquals(1, approvals2.size());
-      assertEquals(1, notesWithApprovals.commentsForBase.size());
+      assertThat(approvals2).hasSize(1);
+      assertThat(notesWithApprovals.comments).hasSize(1);
       notesWithApprovals.close();
     } finally {
       batch.close();
@@ -481,12 +484,12 @@
       batch1 = update1.openUpdateInBatch(bru);
       batch1.write(update1, new CommitBuilder());
       batch1.commit();
-      assertNull(repo.getRef(update1.getRefName()));
+      assertThat(repo.getRef(update1.getRefName())).isNull();
 
       batch2 = update2.openUpdateInBatch(bru);
       batch2.write(update2, new CommitBuilder());
       batch2.commit();
-      assertNull(repo.getRef(update2.getRefName()));
+      assertThat(repo.getRef(update2.getRefName())).isNull();
     } finally {
       if (batch1 != null) {
         batch1.close();
@@ -497,19 +500,19 @@
     }
 
     List<ReceiveCommand> cmds = bru.getCommands();
-    assertEquals(2, cmds.size());
-    assertEquals(update1.getRefName(), cmds.get(0).getRefName());
-    assertEquals(update2.getRefName(), cmds.get(1).getRefName());
+    assertThat(cmds).hasSize(2);
+    assertThat(cmds.get(0).getRefName()).isEqualTo(update1.getRefName());
+    assertThat(cmds.get(1).getRefName()).isEqualTo(update2.getRefName());
 
     try (RevWalk rw = new RevWalk(repo)) {
       bru.execute(rw, NullProgressMonitor.INSTANCE);
     }
 
-    assertEquals(ReceiveCommand.Result.OK, cmds.get(0).getResult());
-    assertEquals(ReceiveCommand.Result.OK, cmds.get(1).getResult());
+    assertThat(cmds.get(0).getResult()).isEqualTo(ReceiveCommand.Result.OK);
+    assertThat(cmds.get(1).getResult()).isEqualTo(ReceiveCommand.Result.OK);
 
-    assertNotNull(repo.getRef(update1.getRefName()));
-    assertNotNull(repo.getRef(update2.getRefName()));
+    assertThat(repo.getRef(update1.getRefName())).isNotNull();
+    assertThat(repo.getRef(update2.getRefName())).isNotNull();
   }
 
   @Test
@@ -524,14 +527,12 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
+    assertThat(changeMessages.keySet()).containsExactly(ps1);
 
     ChangeMessage cm = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("Just a little code change.\n",
-        cm.getMessage());
-    assertEquals(changeOwner.getAccount().getId(),
-        cm.getAuthor());
-    assertEquals(ps1, cm.getPatchSetId());
+    assertThat(cm.getMessage()).isEqualTo("Just a little code change.\n");
+    assertThat(cm.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm.getPatchSetId()).isEqualTo(ps1);
   }
 
   @Test
@@ -542,9 +543,7 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
-        notes.getChangeMessages();
-    assertEquals(0, changeMessages.keySet().size());
+    assertThat(notes.getChangeMessages()).isEmpty();
   }
 
   @Test
@@ -559,11 +558,11 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
+    assertThat(changeMessages).hasSize(1);
 
     ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("Testing trailing double newline\n" + "\n", cm1.getMessage());
-    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
+    assertThat(cm1.getMessage()).isEqualTo("Testing trailing double newline\n" + "\n");
+    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
   }
 
   @Test
@@ -581,15 +580,15 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
+    assertThat(changeMessages).hasSize(1);
 
     ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("Testing paragraph 1\n"
+    assertThat(cm1.getMessage()).isEqualTo("Testing paragraph 1\n"
         + "\n"
         + "Testing paragraph 2\n"
         + "\n"
-        + "Testing paragraph 3", cm1.getMessage());
-    assertEquals(changeOwner.getAccount().getId(), cm1.getAuthor());
+        + "Testing paragraph 3");
+    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
   }
 
   @Test
@@ -611,20 +610,19 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(2, changeMessages.keySet().size());
+    assertThat(changeMessages).hasSize(2);
 
     ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertEquals("This is the change message for the first PS.",
-        cm1.getMessage());
-    assertEquals(changeOwner.getAccount().getId(),
-        cm1.getAuthor());
+    assertThat(cm1.getMessage())
+        .isEqualTo("This is the change message for the first PS.");
+    assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
 
     ChangeMessage cm2 = Iterables.getOnlyElement(changeMessages.get(ps2));
-    assertEquals(ps1, cm1.getPatchSetId());
-    assertEquals("This is the change message for the second PS.",
-        cm2.getMessage());
-    assertEquals(changeOwner.getAccount().getId(), cm2.getAuthor());
-    assertEquals(ps2, cm2.getPatchSetId());
+    assertThat(cm1.getPatchSetId()).isEqualTo(ps1);
+    assertThat(cm2.getMessage())
+        .isEqualTo("This is the change message for the second PS.");
+    assertThat(cm2.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm2.getPatchSetId()).isEqualTo(ps2);
   }
 
   @Test
@@ -645,20 +643,18 @@
     ChangeNotes notes = newNotes(c);
     ListMultimap<PatchSet.Id, ChangeMessage> changeMessages =
         notes.getChangeMessages();
-    assertEquals(1, changeMessages.keySet().size());
+    assertThat(changeMessages.keySet()).hasSize(1);
 
     List<ChangeMessage> cm = changeMessages.get(ps1);
-    assertEquals(2, cm.size());
-    assertEquals("First change message.\n",
-        cm.get(0).getMessage());
-    assertEquals(changeOwner.getAccount().getId(),
-        cm.get(0).getAuthor());
-    assertEquals(ps1, cm.get(0).getPatchSetId());
-    assertEquals("Second change message.\n",
-        cm.get(1).getMessage());
-    assertEquals(changeOwner.getAccount().getId(),
-        cm.get(1).getAuthor());
-    assertEquals(ps1, cm.get(1).getPatchSetId());
+    assertThat(cm).hasSize(2);
+    assertThat(cm.get(0).getMessage()).isEqualTo("First change message.\n");
+    assertThat(cm.get(0).getAuthor())
+        .isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm.get(0).getPatchSetId()).isEqualTo(ps1);
+    assertThat(cm.get(1).getMessage()).isEqualTo("Second change message.\n");
+    assertThat(cm.get(1).getAuthor())
+        .isEqualTo(changeOwner.getAccount().getId());
+    assertThat(cm.get(1).getPatchSetId()).isEqualTo(ps1);
   }
 
   @Test
@@ -677,7 +673,7 @@
     Timestamp time3 = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
 
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment1 = newPublishedComment(psId, "file1",
         uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
         (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -686,7 +682,7 @@
 
     update = newUpdate(c, otherUser);
     CommentRange range2 = new CommentRange(2, 1, 3, 1);
-    PatchLineComment comment2 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment2 = newPublishedComment(psId, "file1",
         uuid2, range2, range2.getEndLine(), otherUser, null, time2, message2,
         (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -695,7 +691,7 @@
 
     update = newUpdate(c, otherUser);
     CommentRange range3 = new CommentRange(3, 1, 4, 1);
-    PatchLineComment comment3 = newPublishedPatchLineComment(psId, "file2",
+    PatchLineComment comment3 = newPublishedComment(psId, "file2",
         uuid3, range3, range3.getEndLine(), otherUser, null, time3, message3,
         (short) 1, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -713,7 +709,7 @@
           walk.getObjectReader().open(
               note.getData(), Constants.OBJ_BLOB).getBytes();
       String noteString = new String(bytes, UTF_8);
-      assertEquals("Patch-set: 1\n"
+      assertThat(noteString).isEqualTo("Patch-set: 1\n"
           + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
           + "File: file1\n"
           + "\n"
@@ -739,8 +735,7 @@
           + "UUID: uuid3\n"
           + "Bytes: 9\n"
           + "comment 3\n"
-          + "\n",
-          noteString);
+          + "\n");
     }
   }
 
@@ -757,7 +752,7 @@
     Timestamp time2 = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
 
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment1 = newPublishedComment(psId, "file1",
         uuid1, range1, range1.getEndLine(), otherUser, null, time1, message1,
         (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -766,7 +761,7 @@
 
     update = newUpdate(c, otherUser);
     CommentRange range2 = new CommentRange(2, 1, 3, 1);
-    PatchLineComment comment2 = newPublishedPatchLineComment(psId, "file1",
+    PatchLineComment comment2 = newPublishedComment(psId, "file1",
         uuid2, range2, range2.getEndLine(), otherUser, null, time2, message2,
         (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
     update.setPatchSetId(psId);
@@ -784,7 +779,7 @@
           walk.getObjectReader().open(
               note.getData(), Constants.OBJ_BLOB).getBytes();
       String noteString = new String(bytes, UTF_8);
-      assertEquals("Base-for-patch-set: 1\n"
+      assertThat(noteString).isEqualTo("Base-for-patch-set: 1\n"
           + "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
           + "File: file1\n"
           + "\n"
@@ -801,8 +796,7 @@
           + "UUID: uuid2\n"
           + "Bytes: 9\n"
           + "comment 2\n"
-          + "\n",
-          noteString);
+          + "\n");
     }
   }
 
@@ -813,6 +807,8 @@
     ChangeUpdate update = newUpdate(c, otherUser);
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     String messageForBase = "comment for base";
     String messageForPS = "comment for ps";
     CommentRange range = new CommentRange(1, 1, 2, 1);
@@ -820,34 +816,26 @@
     PatchSet.Id psId = c.currentPatchSetId();
 
     PatchLineComment commentForBase =
-        newPublishedPatchLineComment(psId, "filename", uuid1,
+        newPublishedComment(psId, "filename", uuid1,
         range, range.getEndLine(), otherUser, null, now, messageForBase,
-        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        (short) 0, rev1);
     update.setPatchSetId(psId);
     update.upsertComment(commentForBase);
     update.commit();
 
     update = newUpdate(c, otherUser);
     PatchLineComment commentForPS =
-        newPublishedPatchLineComment(psId, "filename", uuid2,
+        newPublishedComment(psId, "filename", uuid2,
         range, range.getEndLine(), otherUser, null, now, messageForPS,
-        (short) 1, "abcd4567abcd4567abcd4567abcd4567abcd4567");
+        (short) 1, rev2);
     update.setPatchSetId(psId);
     update.upsertComment(commentForPS);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPS =
-        notes.getPatchSetComments();
-    assertEquals(commentsForBase.size(), 1);
-    assertEquals(commentsForPS.size(), 1);
-
-    assertEquals(commentForBase,
-        Iterables.getOnlyElement(commentsForBase.get(psId)));
-    assertEquals(commentForPS,
-        Iterables.getOnlyElement(commentsForPS.get(psId)));
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(
+            new RevId(rev1), commentForBase,
+            new RevId(rev2), commentForPS));
   }
 
   @Test
@@ -855,6 +843,7 @@
     Change c = newChange();
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id psId = c.currentPatchSetId();
     String filename = "filename";
@@ -863,37 +852,25 @@
     ChangeUpdate update = newUpdate(c, otherUser);
     Timestamp timeForComment1 = TimeUtil.nowTs();
     Timestamp timeForComment2 = TimeUtil.nowTs();
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, filename,
+    PatchLineComment comment1 = newPublishedComment(psId, filename,
         uuid1, range, range.getEndLine(), otherUser, null, timeForComment1,
-        "comment 1", side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        "comment 1", side, rev);
     update.setPatchSetId(psId);
     update.upsertComment(comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
-    PatchLineComment comment2 = newPublishedPatchLineComment(psId, filename,
+    PatchLineComment comment2 = newPublishedComment(psId, filename,
         uuid2, range, range.getEndLine(), otherUser, null, timeForComment2,
-        "comment 2", side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        "comment 2", side, rev);
     update.setPatchSetId(psId);
     update.upsertComment(comment2);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPS =
-        notes.getPatchSetComments();
-    assertEquals(commentsForBase.size(), 0);
-    assertEquals(commentsForPS.size(), 2);
-
-    ImmutableList<PatchLineComment> commentsForThisPS =
-        (ImmutableList<PatchLineComment>) commentsForPS.get(psId);
-    assertEquals(commentsForThisPS.size(), 2);
-    PatchLineComment commentFromNotes1 = commentsForThisPS.get(0);
-    PatchLineComment commentFromNotes2 = commentsForThisPS.get(1);
-
-    assertEquals(comment1, commentFromNotes1);
-    assertEquals(comment2, commentFromNotes2);
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(
+          new RevId(rev), comment1,
+          new RevId(rev), comment2)).inOrder();
   }
 
   @Test
@@ -901,6 +878,7 @@
       throws Exception {
     Change c = newChange();
     String uuid = "uuid";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id psId = c.currentPatchSetId();
     String filename1 = "filename1";
@@ -909,43 +887,33 @@
 
     ChangeUpdate update = newUpdate(c, otherUser);
     Timestamp now = TimeUtil.nowTs();
-    PatchLineComment comment1 = newPublishedPatchLineComment(psId, filename1,
+    PatchLineComment comment1 = newPublishedComment(psId, filename1,
         uuid, range, range.getEndLine(), otherUser, null, now, "comment 1",
-        side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        side, rev);
     update.setPatchSetId(psId);
     update.upsertComment(comment1);
     update.commit();
 
     update = newUpdate(c, otherUser);
-    PatchLineComment comment2 = newPublishedPatchLineComment(psId, filename2,
+    PatchLineComment comment2 = newPublishedComment(psId, filename2,
         uuid, range, range.getEndLine(), otherUser, null, now, "comment 2",
-        side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        side, rev);
     update.setPatchSetId(psId);
     update.upsertComment(comment2);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPS =
-        notes.getPatchSetComments();
-    assertEquals(commentsForBase.size(), 0);
-    assertEquals(commentsForPS.size(), 2);
-
-    ImmutableList<PatchLineComment> commentsForThisPS =
-        (ImmutableList<PatchLineComment>) commentsForPS.get(psId);
-    assertEquals(commentsForThisPS.size(), 2);
-    PatchLineComment commentFromNotes1 = commentsForThisPS.get(0);
-    PatchLineComment commentFromNotes2 = commentsForThisPS.get(1);
-
-    assertEquals(comment1, commentFromNotes1);
-    assertEquals(comment2, commentFromNotes2);
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(
+          new RevId(rev), comment1,
+          new RevId(rev), comment2)).inOrder();
   }
 
   @Test
   public void patchLineCommentMultiplePatchsets() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -953,9 +921,9 @@
 
     ChangeUpdate update = newUpdate(c, otherUser);
     Timestamp now = TimeUtil.nowTs();
-    PatchLineComment comment1 = newPublishedPatchLineComment(ps1, filename,
+    PatchLineComment comment1 = newPublishedComment(ps1, filename,
         uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
-        side, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+        side, rev1);
     update.setPatchSetId(ps1);
     update.upsertComment(comment1);
     update.commit();
@@ -965,37 +933,24 @@
 
     update = newUpdate(c, otherUser);
     now = TimeUtil.nowTs();
-    PatchLineComment comment2 = newPublishedPatchLineComment(ps2, filename,
+    PatchLineComment comment2 = newPublishedComment(ps2, filename,
         uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
-        side, "abcd4567abcd4567abcd4567abcd4567abcd4567");
+        side, rev2);
     update.setPatchSetId(ps2);
     update.upsertComment(comment2);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    LinkedListMultimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        LinkedListMultimap.create(notes.getBaseComments());
-    LinkedListMultimap<PatchSet.Id, PatchLineComment> commentsForPS =
-        LinkedListMultimap.create(notes.getPatchSetComments());
-    assertEquals(commentsForBase.keys().size(), 0);
-    assertEquals(commentsForPS.values().size(), 2);
-
-    List<PatchLineComment> commentsForPS1 = commentsForPS.get(ps1);
-    assertEquals(commentsForPS1.size(), 1);
-    PatchLineComment commentFromPs1 = commentsForPS1.get(0);
-
-    List<PatchLineComment> commentsForPS2 = commentsForPS.get(ps2);
-    assertEquals(commentsForPS2.size(), 1);
-    PatchLineComment commentFromPs2 = commentsForPS2.get(0);
-
-    assertEquals(comment1, commentFromPs1);
-    assertEquals(comment2, commentFromPs2);
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(
+          new RevId(rev1), comment1,
+          new RevId(rev2), comment2));
   }
 
   @Test
   public void patchLineCommentSingleDraftToPublished() throws Exception {
     Change c = newChange();
     String uuid = "uuid";
+    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     CommentRange range = new CommentRange(1, 1, 2, 1);
     PatchSet.Id ps1 = c.currentPatchSetId();
     String filename = "filename1";
@@ -1003,16 +958,17 @@
 
     ChangeUpdate update = newUpdate(c, otherUser);
     Timestamp now = TimeUtil.nowTs();
-    PatchLineComment comment1 = newPatchLineComment(ps1, filename, uuid,
-        range, range.getEndLine(), otherUser, null, now, "comment on ps1", side,
-        "abcd4567abcd4567abcd4567abcd4567abcd4567", Status.DRAFT);
+    PatchLineComment comment1 = newComment(ps1, filename, uuid, range,
+        range.getEndLine(), otherUser, null, now, "comment on ps1", side,
+        rev, Status.DRAFT);
     update.setPatchSetId(ps1);
     update.insertComment(comment1);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertEquals(1, notes.getDraftPsComments(otherUserId).values().size());
-    assertEquals(0, notes.getDraftBaseComments(otherUserId).values().size());
+    assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment1));
+    assertThat(notes.getComments()).isEmpty();
 
     comment1.setStatus(Status.PUBLISHED);
     update = newUpdate(c, otherUser);
@@ -1021,49 +977,44 @@
     update.commit();
 
     notes = newNotes(c);
-
-    assertTrue(notes.getDraftPsComments(otherUserId).values().isEmpty());
-    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
-
-    assertTrue(notes.getBaseComments().values().isEmpty());
-    PatchLineComment commentFromNotes =
-        Iterables.getOnlyElement(notes.getPatchSetComments().values());
-    assertEquals(comment1, commentFromNotes);
+    assertThat(notes.getDraftComments(otherUserId)).isEmpty();
+    assertThat(notes.getComments()).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment1));
   }
 
   @Test
   public void patchLineCommentMultipleDraftsSameSidePublishOne()
-      throws OrmException, IOException {
+      throws Exception {
     Change c = newChange();
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
+    String rev = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     CommentRange range1 = new CommentRange(1, 1, 2, 2);
     CommentRange range2 = new CommentRange(2, 2, 3, 3);
     String filename = "filename1";
     short side = (short) 1;
     Timestamp now = TimeUtil.nowTs();
-    String commitSHA1 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     PatchSet.Id psId = c.currentPatchSetId();
 
     // Write two drafts on the same side of one patch set.
     ChangeUpdate update = newUpdate(c, otherUser);
     update.setPatchSetId(psId);
-    PatchLineComment comment1 = newPatchLineComment(psId, filename, uuid1,
+    PatchLineComment comment1 = newComment(psId, filename, uuid1,
         range1, range1.getEndLine(), otherUser, null, now, "comment on ps1",
-        side, commitSHA1, Status.DRAFT);
-    PatchLineComment comment2 = newPatchLineComment(psId, filename, uuid2,
+        side, rev, Status.DRAFT);
+    PatchLineComment comment2 = newComment(psId, filename, uuid2,
         range2, range2.getEndLine(), otherUser, null, now, "other on ps1",
-        side, commitSHA1, Status.DRAFT);
+        side, rev, Status.DRAFT);
     update.insertComment(comment1);
     update.insertComment(comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
-    assertEquals(2, notes.getDraftPsComments(otherUserId).values().size());
-
-    assertTrue(notes.getDraftPsComments(otherUserId).containsValue(comment1));
-    assertTrue(notes.getDraftPsComments(otherUserId).containsValue(comment2));
+    assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+        ImmutableMultimap.of(
+          new RevId(rev), comment1,
+          new RevId(rev), comment2)).inOrder();
+    assertThat(notes.getComments()).isEmpty();
 
     // Publish first draft.
     update = newUpdate(c, otherUser);
@@ -1073,54 +1024,46 @@
     update.commit();
 
     notes = newNotes(c);
-    assertEquals(comment1,
-        Iterables.getOnlyElement(notes.getPatchSetComments().get(psId)));
-    assertEquals(comment2,
-        Iterables.getOnlyElement(
-            notes.getDraftPsComments(otherUserId).values()));
-
-    assertTrue(notes.getBaseComments().values().isEmpty());
-    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
+    assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment2));
+    assertThat(notes.getComments()).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment1));
   }
 
   @Test
   public void patchLineCommentsMultipleDraftsBothSidesPublishAll()
-      throws OrmException, IOException {
+      throws Exception {
     Change c = newChange();
     String uuid1 = "uuid1";
     String uuid2 = "uuid2";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
     CommentRange range1 = new CommentRange(1, 1, 2, 2);
     CommentRange range2 = new CommentRange(2, 2, 3, 3);
     String filename = "filename1";
     Timestamp now = TimeUtil.nowTs();
-    String commitSHA1 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
-    String baseSHA1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     PatchSet.Id psId = c.currentPatchSetId();
 
     // Write two drafts, one on each side of the patchset.
     ChangeUpdate update = newUpdate(c, otherUser);
     update.setPatchSetId(psId);
-    PatchLineComment baseComment = newPatchLineComment(psId, filename, uuid1,
+    PatchLineComment baseComment = newComment(psId, filename, uuid1,
         range1, range1.getEndLine(), otherUser, null, now, "comment on base",
-        (short) 0, baseSHA1, Status.DRAFT);
-    PatchLineComment psComment = newPatchLineComment(psId, filename, uuid2,
+        (short) 0, rev1, Status.DRAFT);
+    PatchLineComment psComment = newComment(psId, filename, uuid2,
         range2, range2.getEndLine(), otherUser, null, now, "comment on ps",
-        (short) 1, commitSHA1, Status.DRAFT);
+        (short) 1, rev2, Status.DRAFT);
 
     update.insertComment(baseComment);
     update.insertComment(psComment);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    PatchLineComment baseDraftCommentFromNotes =
-        Iterables.getOnlyElement(
-            notes.getDraftBaseComments(otherUserId).values());
-    PatchLineComment psDraftCommentFromNotes =
-        Iterables.getOnlyElement(
-            notes.getDraftPsComments(otherUserId).values());
-
-    assertEquals(baseComment, baseDraftCommentFromNotes);
-    assertEquals(psComment, psDraftCommentFromNotes);
+    assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+        ImmutableMultimap.of(
+            new RevId(rev1), baseComment,
+            new RevId(rev2), psComment));
+    assertThat(notes.getComments()).isEmpty();
 
     // Publish both comments.
     update = newUpdate(c, otherUser);
@@ -1133,17 +1076,98 @@
     update.commit();
 
     notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).isEmpty();
+    assertThat(notes.getComments()).containsExactly(
+        ImmutableMultimap.of(
+            new RevId(rev1), baseComment,
+            new RevId(rev2), psComment));
+  }
 
-    PatchLineComment baseCommentFromNotes =
-        Iterables.getOnlyElement(notes.getBaseComments().values());
-    PatchLineComment psCommentFromNotes =
-        Iterables.getOnlyElement(notes.getPatchSetComments().values());
+  @Test
+  public void patchLineCommentsDeleteAllDrafts() throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    ObjectId objId = ObjectId.fromString(rev);
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id psId = c.currentPatchSetId();
+    String filename = "filename";
+    short side = (short) 1;
 
-    assertEquals(baseComment, baseCommentFromNotes);
-    assertEquals(psComment, psCommentFromNotes);
+    ChangeUpdate update = newUpdate(c, otherUser);
+    Timestamp now = TimeUtil.nowTs();
+    PatchLineComment comment = newComment(psId, filename, uuid, range,
+        range.getEndLine(), otherUser, null, now, "comment on ps1", side,
+        rev, Status.DRAFT);
+    update.setPatchSetId(psId);
+    update.upsertComment(comment);
+    update.commit();
 
-    assertTrue(notes.getDraftBaseComments(otherUserId).values().isEmpty());
-    assertTrue(notes.getDraftPsComments(otherUserId).values().isEmpty());
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).hasSize(1);
+    assertThat(notes.getDraftCommentNotes().getNoteMap().contains(objId))
+      .isTrue();
+
+    update = newUpdate(c, otherUser);
+    now = TimeUtil.nowTs();
+    update.setPatchSetId(psId);
+    update.deleteComment(comment);
+    update.commit();
+
+    notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).isEmpty();
+    assertThat(notes.getDraftCommentNotes().getNoteMap()).isNull();
+  }
+
+  @Test
+  public void patchLineCommentsDeleteAllDraftsForOneRevision()
+      throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    ObjectId objId1 = ObjectId.fromString(rev1);
+    ObjectId objId2 = ObjectId.fromString(rev2);
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id ps1 = c.currentPatchSetId();
+    String filename = "filename1";
+    short side = (short) 1;
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    Timestamp now = TimeUtil.nowTs();
+    PatchLineComment comment1 = newComment(ps1, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
+        side, rev1, Status.DRAFT);
+    update.setPatchSetId(ps1);
+    update.upsertComment(comment1);
+    update.commit();
+
+    incrementPatchSet(c);
+    PatchSet.Id ps2 = c.currentPatchSetId();
+
+    update = newUpdate(c, otherUser);
+    now = TimeUtil.nowTs();
+    PatchLineComment comment2 = newComment(ps2, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
+        side, rev2, Status.DRAFT);
+    update.setPatchSetId(ps2);
+    update.upsertComment(comment2);
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).hasSize(2);
+
+    update = newUpdate(c, otherUser);
+    now = TimeUtil.nowTs();
+    update.setPatchSetId(ps2);
+    update.deleteComment(comment2);
+    update.commit();
+
+    notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).hasSize(1);
+    NoteMap noteMap = notes.getDraftCommentNotes().getNoteMap();
+    assertThat(noteMap.contains(objId1)).isTrue();
+    assertThat(noteMap.contains(objId2)).isFalse();
   }
 
   @Test
@@ -1151,27 +1175,20 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     String uuid = "uuid";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     String messageForBase = "comment for base";
     Timestamp now = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
 
-    PatchLineComment commentForBase =
-        newPublishedPatchLineComment(psId, "filename", uuid,
-        null, 0, otherUser, null, now, messageForBase,
-        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    PatchLineComment comment = newPublishedComment(
+        psId, "filename", uuid, null, 0, otherUser, null, now, messageForBase,
+        (short) 0, rev);
     update.setPatchSetId(psId);
-    update.upsertComment(commentForBase);
+    update.upsertComment(comment);
     update.commit();
 
-    ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPs =
-        notes.getPatchSetComments();
-
-    assertTrue(commentsForPs.isEmpty());
-    assertEquals(commentForBase,
-        Iterables.getOnlyElement(commentsForBase.get(psId)));
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment));
   }
 
   @Test
@@ -1179,26 +1196,63 @@
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, otherUser);
     String uuid = "uuid";
+    String rev = "abcd1234abcd1234abcd1234abcd1234abcd1234";
     String messageForBase = "comment for base";
     Timestamp now = TimeUtil.nowTs();
     PatchSet.Id psId = c.currentPatchSetId();
 
-    PatchLineComment commentForBase =
-        newPublishedPatchLineComment(psId, "filename", uuid,
-        null, 1, otherUser, null, now, messageForBase,
-        (short) 0, "abcd1234abcd1234abcd1234abcd1234abcd1234");
+    PatchLineComment comment = newPublishedComment(
+        psId, "filename", uuid, null, 1, otherUser, null, now, messageForBase,
+        (short) 0, rev);
     update.setPatchSetId(psId);
-    update.upsertComment(commentForBase);
+    update.upsertComment(comment);
+    update.commit();
+
+    assertThat(newNotes(c).getComments()).containsExactly(
+        ImmutableMultimap.of(new RevId(rev), comment));
+  }
+
+  @Test
+  public void updateCommentsForMultipleRevisions() throws Exception {
+    Change c = newChange();
+    String uuid = "uuid";
+    String rev1 = "abcd1234abcd1234abcd1234abcd1234abcd1234";
+    String rev2 = "abcd4567abcd4567abcd4567abcd4567abcd4567";
+    CommentRange range = new CommentRange(1, 1, 2, 1);
+    PatchSet.Id ps1 = c.currentPatchSetId();
+    String filename = "filename1";
+    short side = (short) 1;
+
+    incrementPatchSet(c);
+    PatchSet.Id ps2 = c.currentPatchSetId();
+
+    ChangeUpdate update = newUpdate(c, otherUser);
+    update.setPatchSetId(ps2);
+    Timestamp now = TimeUtil.nowTs();
+    PatchLineComment comment1 = newComment(ps1, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps1",
+        side, rev1, Status.DRAFT);
+    PatchLineComment comment2 = newComment(ps2, filename,
+        uuid, range, range.getEndLine(), otherUser, null, now, "comment on ps2",
+        side, rev2, Status.DRAFT);
+    update.upsertComment(comment1);
+    update.upsertComment(comment2);
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    Multimap<PatchSet.Id, PatchLineComment> commentsForBase =
-        notes.getBaseComments();
-    Multimap<PatchSet.Id, PatchLineComment> commentsForPs =
-        notes.getPatchSetComments();
+    assertThat(notes.getDraftComments(otherUserId)).hasSize(2);
+    assertThat(notes.getComments()).isEmpty();
 
-    assertTrue(commentsForPs.isEmpty());
-    assertEquals(commentForBase,
-        Iterables.getOnlyElement(commentsForBase.get(psId)));
+    update = newUpdate(c, otherUser);
+    update.setPatchSetId(ps2);
+    comment1.setStatus(Status.PUBLISHED);
+    comment2.setStatus(Status.PUBLISHED);
+    update.upsertComment(comment1);
+    update.upsertComment(comment2);
+    update.commit();
+
+    notes = newNotes(c);
+    assertThat(notes.getDraftComments(otherUserId)).isEmpty();
+    assertThat(notes.getComments()).hasSize(2);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 328509a..49b61cc 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.notedb;
 
+import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.notedb.ReviewerState.CC;
 import static com.google.gerrit.server.notedb.ReviewerState.REVIEWER;
-import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.TimeUtil;
@@ -43,7 +43,7 @@
     update.putReviewer(changeOwner.getAccount().getId(), REVIEWER);
     update.putReviewer(otherUser.getAccount().getId(), CC);
     update.commit();
-    assertEquals("refs/changes/01/1/meta", update.getRefName());
+    assertThat(update.getRefName()).isEqualTo("refs/changes/01/1/meta");
 
     RevCommit commit = parseCommit(update.getRevision());
     assertBodyEquals("Update patch set 1\n"
@@ -56,17 +56,18 @@
         commit);
 
     PersonIdent author = commit.getAuthorIdent();
-    assertEquals("Change Owner", author.getName());
-    assertEquals("1@gerrit", author.getEmailAddress());
-    assertEquals(new Date(c.getCreatedOn().getTime() + 1000),
-        author.getWhen());
-    assertEquals(TimeZone.getTimeZone("GMT-7:00"), author.getTimeZone());
+    assertThat(author.getName()).isEqualTo("Change Owner");
+    assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
+    assertThat(author.getWhen())
+        .isEqualTo(new Date(c.getCreatedOn().getTime() + 1000));
+    assertThat(author.getTimeZone())
+        .isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
 
     PersonIdent committer = commit.getCommitterIdent();
-    assertEquals("Gerrit Server", committer.getName());
-    assertEquals("noreply@gerrit.com", committer.getEmailAddress());
-    assertEquals(author.getWhen(), committer.getWhen());
-    assertEquals(author.getTimeZone(), committer.getTimeZone());
+    assertThat(committer.getName()).isEqualTo("Gerrit Server");
+    assertThat(committer.getEmailAddress()).isEqualTo("noreply@gerrit.com");
+    assertThat(committer.getWhen()).isEqualTo(author.getWhen());
+    assertThat(committer.getTimeZone()).isEqualTo(author.getTimeZone());
   }
 
   @Test
@@ -76,7 +77,7 @@
     update.setChangeMessage("Just a little code change.\n"
         + "How about a new line");
     update.commit();
-    assertEquals("refs/changes/01/1/meta", update.getRefName());
+    assertThat(update.getRefName()).isEqualTo("refs/changes/01/1/meta");
 
     assertBodyEquals("Update patch set 1\n"
         + "\n"
@@ -130,17 +131,18 @@
         commit);
 
     PersonIdent author = commit.getAuthorIdent();
-    assertEquals("Change Owner", author.getName());
-    assertEquals("1@gerrit", author.getEmailAddress());
-    assertEquals(new Date(c.getCreatedOn().getTime() + 1000),
-        author.getWhen());
-    assertEquals(TimeZone.getTimeZone("GMT-7:00"), author.getTimeZone());
+    assertThat(author.getName()).isEqualTo("Change Owner");
+    assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
+    assertThat(author.getWhen())
+        .isEqualTo(new Date(c.getCreatedOn().getTime() + 1000));
+    assertThat(author.getTimeZone())
+        .isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
 
     PersonIdent committer = commit.getCommitterIdent();
-    assertEquals("Gerrit Server", committer.getName());
-    assertEquals("noreply@gerrit.com", committer.getEmailAddress());
-    assertEquals(author.getWhen(), committer.getWhen());
-    assertEquals(author.getTimeZone(), committer.getTimeZone());
+    assertThat(committer.getName()).isEqualTo("Gerrit Server");
+    assertThat(committer.getEmailAddress()).isEqualTo("noreply@gerrit.com");
+    assertThat(committer.getWhen()).isEqualTo(author.getWhen());
+    assertThat(committer.getTimeZone()).isEqualTo(author.getTimeZone());
   }
 
   @Test
@@ -161,8 +163,8 @@
         commit);
 
     PersonIdent author = commit.getAuthorIdent();
-    assertEquals("Anonymous Coward (3)", author.getName());
-    assertEquals("3@gerrit", author.getEmailAddress());
+    assertThat(author.getName()).isEqualTo("Anonymous Coward (3)");
+    assertThat(author.getEmailAddress()).isEqualTo("3@gerrit");
   }
 
   @Test
@@ -252,6 +254,6 @@
   private void assertBodyEquals(String expected, ObjectId commitId)
       throws Exception {
     RevCommit commit = parseCommit(commitId);
-    assertEquals(expected, commit.getFullMessage());
+    assertThat(commit.getFullMessage()).isEqualTo(expected);
   }
 }
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 85e06e8..fd36097 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
@@ -59,12 +59,12 @@
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
+import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -191,7 +191,7 @@
 
   @Test
   public void byId() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
 
@@ -202,7 +202,7 @@
 
   @Test
   public void byKey() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change = newChange(repo, null, null, null, null).insert();
     String key = change.getKey().get();
 
@@ -215,7 +215,7 @@
 
   @Test
   public void byTriplet() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change = newChange(repo, null, null, null, "branch").insert();
     String k = change.getKey().get();
 
@@ -238,7 +238,7 @@
 
   @Test
   public void byStatus() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.NEW);
@@ -257,7 +257,7 @@
 
   @Test
   public void byStatusOpen() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.NEW);
@@ -287,7 +287,7 @@
 
   @Test
   public void byStatusClosed() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.MERGED);
@@ -315,7 +315,7 @@
 
   @Test
   public void byStatusPrefix() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setStatus(Change.Status.NEW);
@@ -337,7 +337,7 @@
 
   @Test
   public void byCommit() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
     ins.insert();
     String sha = ins.getPatchSet().getRevision().get();
@@ -351,7 +351,7 @@
 
   @Test
   public void byOwner() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
@@ -363,7 +363,7 @@
 
   @Test
   public void byOwnerIn() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
@@ -375,8 +375,8 @@
 
   @Test
   public void byProject() throws Exception {
-    TestRepository<InMemoryRepository> repo1 = createProject("repo1");
-    TestRepository<InMemoryRepository> repo2 = createProject("repo2");
+    TestRepository<Repo> repo1 = createProject("repo1");
+    TestRepository<Repo> repo2 = createProject("repo2");
     Change change1 = newChange(repo1, null, null, null, null).insert();
     Change change2 = newChange(repo2, null, null, null, null).insert();
 
@@ -388,8 +388,8 @@
 
   @Test
   public void byProjectPrefix() throws Exception {
-    TestRepository<InMemoryRepository> repo1 = createProject("repo1");
-    TestRepository<InMemoryRepository> repo2 = createProject("repo2");
+    TestRepository<Repo> repo1 = createProject("repo1");
+    TestRepository<Repo> repo2 = createProject("repo2");
     Change change1 = newChange(repo1, null, null, null, null).insert();
     Change change2 = newChange(repo2, null, null, null, null).insert();
 
@@ -401,7 +401,7 @@
 
   @Test
   public void byBranchAndRef() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, "master").insert();
     Change change2 = newChange(repo, null, null, null, "branch").insert();
 
@@ -419,7 +419,7 @@
 
   @Test
   public void byTopic() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.getChange();
     change1.setTopic("feature1");
@@ -441,7 +441,7 @@
 
   @Test
   public void byMessageExact() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 = repo.parseBody(repo.commit().message("one").create());
     Change change1 = newChange(repo, commit1, null, null, null).insert();
     RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
@@ -454,7 +454,7 @@
 
   @Test
   public void fullTextWithNumbers() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 =
         repo.parseBody(repo.commit().message("12345 67890").create());
     Change change1 = newChange(repo, commit1, null, null, null).insert();
@@ -470,7 +470,7 @@
   @Test
   public void byLabel() throws Exception {
     accountManager.authenticate(AuthRequest.forUser("anotheruser"));
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
     Change change = ins.insert();
 
@@ -510,7 +510,7 @@
 
   @Test
   public void limit() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change last = null;
     int n = 5;
     for (int i = 0; i < n; i++) {
@@ -538,7 +538,7 @@
 
   @Test
   public void start() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 2; i++) {
       changes.add(newChange(repo, null, null, null, null).insert());
@@ -552,7 +552,7 @@
 
   @Test
   public void startWithLimit() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 3; i++) {
       changes.add(newChange(repo, null, null, null, null).insert());
@@ -568,7 +568,7 @@
 
   @Test
   public void maxPages() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change = newChange(repo, null, null, null, null).insert();
 
     QueryRequest query = newQuery("status:new").withLimit(10);
@@ -582,7 +582,7 @@
   @Test
   public void updateOrder() throws Exception {
     clockStepMs = MILLISECONDS.convert(2, MINUTES);
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     List<ChangeInserter> inserters = Lists.newArrayList();
     List<Change> changes = Lists.newArrayList();
     for (int i = 0; i < 5; i++) {
@@ -607,7 +607,7 @@
   @Test
   public void updatedOrderWithMinuteResolution() throws Exception {
     clockStepMs = MILLISECONDS.convert(2, MINUTES);
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
@@ -629,7 +629,7 @@
 
   @Test
   public void updatedOrderWithSubMinuteResolution() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
@@ -652,7 +652,7 @@
 
   @Test
   public void filterOutMoreThanOnePageOfResults() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change = newChange(repo, null, null, userId.get(), null).insert();
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
@@ -666,7 +666,7 @@
 
   @Test
   public void filterOutAllResults() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
         .getAccountId().get();
     for (int i = 0; i < 5; i++) {
@@ -679,7 +679,7 @@
 
   @Test
   public void byFileExact() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
@@ -696,7 +696,7 @@
 
   @Test
   public void byFileRegex() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
@@ -710,7 +710,7 @@
 
   @Test
   public void byPathExact() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
@@ -727,7 +727,7 @@
 
   @Test
   public void byPathRegex() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit = repo.parseBody(
         repo.commit().message("one")
         .add("dir/file1", "contents1").add("dir/file2", "contents2")
@@ -740,7 +740,7 @@
 
   @Test
   public void byComment() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins = newChange(repo, null, null, null, null);
     Change change = ins.insert();
 
@@ -762,7 +762,7 @@
   public void byAge() throws Exception {
     long thirtyHours = MILLISECONDS.convert(30, HOURS);
     clockStepMs = thirtyHours;
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
     clockStepMs = 0; // Queried by AgePredicate constructor.
@@ -784,7 +784,7 @@
   @Test
   public void byBefore() throws Exception {
     clockStepMs = MILLISECONDS.convert(30, HOURS);
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
     clockStepMs = 0;
@@ -804,7 +804,7 @@
   @Test
   public void byAfter() throws Exception {
     clockStepMs = MILLISECONDS.convert(30, HOURS);
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
     clockStepMs = 0;
@@ -818,7 +818,7 @@
 
   @Test
   public void bySize() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
 
     // added = 3, deleted = 0, delta = 3
     RevCommit commit1 = repo.parseBody(
@@ -855,7 +855,7 @@
   }
 
   private List<Change> setUpHashtagChanges() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
 
@@ -897,7 +897,7 @@
 
   @Test
   public void byDefault() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
 
     Change change1 = newChange(repo, null, null, null, null).insert();
 
@@ -943,7 +943,7 @@
 
   @Test
   public void implicitVisibleTo() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();
     ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
     Change change2 = ins2.getChange();
@@ -961,7 +961,7 @@
 
   @Test
   public void explicitVisibleTo() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, userId.get(), null).insert();
     ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
     Change change2 = ins2.getChange();
@@ -980,7 +980,7 @@
 
   @Test
   public void byCommentBy() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
     Change change2 = newChange(repo, null, null, null, null).insert();
 
@@ -1006,7 +1006,7 @@
 
   @Test
   public void byFrom() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     Change change1 = newChange(repo, null, null, null, null).insert();
 
     int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
@@ -1029,7 +1029,7 @@
 
   @Test
   public void conflicts() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 = repo.parseBody(
         repo.commit()
             .add("file1", "contents1")
@@ -1060,7 +1060,7 @@
   }
 
   protected ChangeInserter newChange(
-      TestRepository<InMemoryRepository> repo,
+      TestRepository<Repo> repo,
       @Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,
       @Nullable String branch) throws Exception {
     if (commit == null) {
@@ -1108,8 +1108,7 @@
     }
   }
 
-  protected TestRepository<InMemoryRepository> createProject(String name)
-      throws Exception {
+  protected TestRepository<Repo> createProject(String name) throws Exception {
     gApi.projects().create(name).get();
     return new TestRepository<>(
         repoManager.openRepository(new Project.NameKey(name)));
@@ -1126,8 +1125,7 @@
 
   protected void assertQuery(QueryRequest query, Change... changes)
       throws Exception {
-    assertThat((Iterable<Integer>) query(query))
-        .named(query.toString())
+    assertThat(query(query)).named(query.toString())
         .containsExactlyElementsIn(ids(changes)).inOrder();
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/ChangeDataTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/ChangeDataTest.java
new file mode 100644
index 0000000..ca1e2b1
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/ChangeDataTest.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.testutil.TestChanges;
+
+import org.junit.Test;
+
+public class ChangeDataTest {
+  @Test
+  public void setPatchSetsClearsCurrentPatchSet() throws Exception {
+    ChangeData cd = ChangeData.createForTest(new Change.Id(1), 1);
+    cd.setChange(TestChanges.newChange(
+          new Project.NameKey("project"), new Account.Id(1000)));
+    PatchSet curr1 = cd.currentPatchSet();
+    int currId = curr1.getId().get();
+    PatchSet ps1 = new PatchSet(new PatchSet.Id(cd.getId(), currId + 1));
+    PatchSet ps2 = new PatchSet(new PatchSet.Id(cd.getId(), currId + 2));
+    cd.setPatchSets(ImmutableList.of(ps1, ps2));
+    PatchSet curr2 = cd.currentPatchSet();
+    assertThat(curr2).isNotSameAs(curr1);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index 5627d33..6122d65 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -16,10 +16,10 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.testutil.InMemoryModule;
+import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -35,7 +35,7 @@
 
   @Test
   public void fullTextWithSpecialChars() throws Exception {
-    TestRepository<InMemoryRepository> repo = createProject("repo");
+    TestRepository<Repo> repo = createProject("repo");
     RevCommit commit1 =
         repo.parseBody(repo.commit().message("foo_bar_foo").create());
     Change change1 = newChange(repo, commit1, null, null, null).insert();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index 2b8f522..bd672f3 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -21,6 +21,7 @@
 import static org.easymock.EasyMock.verify;
 import static org.junit.Assert.assertEquals;
 
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
@@ -34,10 +35,10 @@
 import org.junit.Test;
 
 import java.net.URI;
-import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
 
 public class SubmoduleSectionParserTest extends LocalDiskRepositoryTestCase {
@@ -87,7 +88,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    List<SubmoduleSubscription> expectedSubscriptions = new ArrayList<>();
+    Set<SubmoduleSubscription> expectedSubscriptions = Sets.newHashSet();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a"), "refs/heads/master"), "a"));
@@ -134,7 +135,7 @@
         new Branch.NameKey(new Project.NameKey("super-project"),
             "refs/heads/master");
 
-    List<SubmoduleSubscription> expectedSubscriptions = new ArrayList<>();
+    Set<SubmoduleSubscription> expectedSubscriptions = Sets.newHashSet();
     expectedSubscriptions
         .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
             new Project.NameKey("a"), "refs/heads/master"), "a"));
@@ -159,9 +160,10 @@
     sectionsToReturn.put("a", new SubmoduleSection("ssh://review.source.com/a",
         "a", "."));
 
+    Set<SubmoduleSubscription> expectedSubscriptions = Collections.emptySet();
     execute(new Branch.NameKey(new Project.NameKey("super-project"),
         "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
-        new ArrayList<SubmoduleSubscription>());
+        expectedSubscriptions);
   }
 
   @Test
@@ -170,9 +172,10 @@
     sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
         "."));
 
+    Set<SubmoduleSubscription> expectedSubscriptions = Collections.emptySet();
     execute(new Branch.NameKey(new Project.NameKey("super-project"),
         "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
-        new ArrayList<SubmoduleSubscription>());
+        expectedSubscriptions);
   }
 
   @Test
@@ -181,15 +184,16 @@
     sectionsToReturn.put("project", new SubmoduleSection(
         "ssh://localhost/company/tools/project", "project", "."));
 
+    Set<SubmoduleSubscription> expectedSubscriptions = Collections.emptySet();
     execute(new Branch.NameKey(new Project.NameKey("super-project"),
         "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
-        new ArrayList<SubmoduleSubscription>());
+        expectedSubscriptions);
   }
 
   private void execute(final Branch.NameKey superProjectBranch,
       final Map<String, SubmoduleSection> sectionsToReturn,
       final Map<String, String> reposToBeFound,
-      final List<SubmoduleSubscription> expectedSubscriptions) throws Exception {
+      final Set<SubmoduleSubscription> expectedSubscriptions) throws Exception {
     expect(bbc.getSubsections("submodule"))
         .andReturn(sectionsToReturn.keySet());
 
@@ -231,7 +235,7 @@
         new SubmoduleSectionParser(projectCache, bbc, THIS_SERVER,
             superProjectBranch);
 
-    List<SubmoduleSubscription> returnedSubscriptions = ssp.parseAllSections();
+    Set<SubmoduleSubscription> returnedSubscriptions = ssp.parseAllSections();
 
     doVerify();
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
index b8d5cd2..7adf721 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.testutil;
 
 import com.google.auto.value.AutoValue;
+import com.google.common.base.Predicate;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.common.errors.EmailException;
@@ -102,6 +104,19 @@
     }
   }
 
+  public ImmutableList<Message> getMessages(String changeId, String type) {
+    final String idFooter = "\nGerrit-Change-Id: " + changeId + "\n";
+    final String typeFooter = "\nGerrit-MessageType: " + type + "\n";
+    return FluentIterable.from(getMessages())
+        .filter(new Predicate<Message>() {
+          @Override
+          public boolean apply(Message in) {
+            return in.body().contains(idFooter)
+                && in.body().contains(typeFooter);
+          }
+        }).toList();
+  }
+
   private void waitForEmails() {
     // TODO(dborowitz): This is brittle; consider forcing emails to use
     // a single thread in tests (tricky because most callers just use the
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index c89df6b..2219d28 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -39,11 +39,11 @@
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.config.TrackingFootersProvider;
+import com.google.gerrit.server.git.ChangeCacheImplModule;
 import com.google.gerrit.server.git.EmailReviewCommentsExecutor;
 import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.PerThreadRequestScope;
-import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexModule.IndexType;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
@@ -93,6 +93,7 @@
     cfg.setBoolean("index", "lucene", "testInmemory", true);
     cfg.setInt("index", "lucene", "testVersion",
         ChangeSchemas.getLatest().getVersion());
+    cfg.setInt("sendemail", null, "threadPoolSize", 0);
   }
 
   private final Config cfg;
@@ -128,6 +129,7 @@
       }
     });
     install(cfgInjector.getInstance(GerritGlobalModule.class));
+    install(new ChangeCacheImplModule(false));
     factory(GarbageCollection.Factory.class);
 
     bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
@@ -203,10 +205,8 @@
   @Provides
   @Singleton
   @EmailReviewCommentsExecutor
-  public WorkQueue.Executor createEmailReviewCommentsExecutor(
-      @GerritServerConfig Config config, WorkQueue queues) {
-    int poolSize = config.getInt("sendemail", null, "threadPoolSize", 1);
-    return queues.createQueue(poolSize, "EmailReviewComments");
+  public ExecutorService createEmailReviewCommentsExecutor() {
+    return MoreExecutors.newDirectExecutorService();
   }
 
   @Provides
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
index 385a49ec..ec53b29 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -35,16 +35,22 @@
     return new Repo(name);
   }
 
-  private static class Description extends DfsRepositoryDescription {
+  public static class Description extends DfsRepositoryDescription {
+    private final Project.NameKey name;
     private String desc;
 
     private Description(Project.NameKey name) {
       super(name.get());
+      this.name = name;
       desc = "In-memory repository " + name.get();
     }
+
+    public Project.NameKey getProject() {
+      return name;
+    }
   }
 
-  private static class Repo extends InMemoryRepository {
+  public static class Repo extends InMemoryRepository {
     private Repo(Project.NameKey name) {
       super(new Description(name));
     }
@@ -58,13 +64,13 @@
   private Map<String, Repo> repos = Maps.newHashMap();
 
   @Override
-  public synchronized InMemoryRepository openRepository(Project.NameKey name)
+  public synchronized Repo openRepository(Project.NameKey name)
       throws RepositoryNotFoundException {
     return get(name);
   }
 
   @Override
-  public synchronized InMemoryRepository createRepository(Project.NameKey name)
+  public synchronized Repo createRepository(Project.NameKey name)
       throws RepositoryCaseMismatchException, RepositoryNotFoundException {
     Repo repo;
     try {
@@ -80,7 +86,7 @@
   }
 
   @Override
-  public synchronized InMemoryRepository openMetadataRepository(
+  public synchronized Repo openMetadataRepository(
       Project.NameKey name) throws RepositoryNotFoundException {
     return openRepository(name);
   }
diff --git a/gerrit-sshd/BUCK b/gerrit-sshd/BUCK
index 4774cb3..7bee47e 100644
--- a/gerrit-sshd/BUCK
+++ b/gerrit-sshd/BUCK
@@ -28,6 +28,7 @@
     '//lib/mina:core',
     '//lib/mina:sshd',
     '//lib/jgit:jgit',
+    '//lib/jgit:jgit-archive',
   ],
   provided_deps = [
     '//lib/bouncycastle:bcprov',
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index e4b21d1..2d071c7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -70,6 +70,8 @@
     // Honor the legacy hyphenated forms as aliases for the non-hyphenated forms
     command("git-upload-pack").to(Commands.key(git, "upload-pack"));
     command(git, "upload-pack").to(Upload.class);
+    command("git-upload-archive").to(Commands.key(git, "upload-archive"));
+    command(git, "upload-archive").to(UploadArchive.class);
     command("suexec").to(SuExec.class);
     listener().to(ShowCaches.StartupListener.class);
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index c41fcdc..15e030f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
@@ -110,7 +110,7 @@
               db, true).filter(repo.getRefDatabase().getRefs(ALL), false);
 
       for (final String ref : refsMap.keySet()) {
-        if (!onlyRefsHeads || ref.startsWith(Branch.R_HEADS)) {
+        if (!onlyRefsHeads || ref.startsWith(RefNames.REFS_HEADS)) {
           stdout.println(ref);
         }
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index c3c710d..d45d76e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@RequiresCapability(GlobalCapability.VIEW_PLUGINS)
 @CommandMetaData(name = "ls", description = "List the installed plugins",
   runsAt = MASTER_OR_SLAVE)
 final class PluginLsCommand extends SshCommand {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
new file mode 100644
index 0000000..929f7ea
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -0,0 +1,226 @@
+// Copyright (C) 2014 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.sshd.commands;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.ArchiveFormat;
+import com.google.gerrit.server.change.GetArchive;
+import com.google.gerrit.sshd.AbstractGitCommand;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.api.ArchiveCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PacketLineIn;
+import org.eclipse.jgit.transport.PacketLineOut;
+import org.eclipse.jgit.transport.SideBandOutputStream;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Allows getting archives for Git repositories over SSH using the Git
+ * upload-archive protocol.
+ */
+public class UploadArchive extends AbstractGitCommand {
+  /**
+   * Options for parsing Git commands.
+   * <p>
+   * These options are not passed on command line, but received through input
+   * stream in pkt-line format.
+   */
+  static class Options {
+    @Option(name = "-f", aliases = {"--format"}, usage = "Format of the"
+        + " resulting archive: tar or zip... If this option is not given, and"
+        + " the output file is specified, the format is inferred from the"
+        + " filename if possible (e.g. writing to \"foo.zip\" makes the output"
+        + " to be in the zip format). Otherwise the output format is tar.")
+    private String format = "tar";
+
+    @Option(name = "--prefix",
+        usage = "Prepend <prefix>/ to each filename in the archive.")
+    private String prefix;
+
+    @Option(name = "-0", usage = "Store the files instead of deflating them.")
+    private boolean level0;
+    @Option(name = "-1")
+    private boolean level1;
+    @Option(name = "-2")
+    private boolean level2;
+    @Option(name = "-3")
+    private boolean level3;
+    @Option(name = "-4")
+    private boolean level4;
+    @Option(name = "-5")
+    private boolean level5;
+    @Option(name = "-6")
+    private boolean level6;
+    @Option(name = "-7")
+    private boolean level7;
+    @Option(name = "-8")
+    private boolean level8;
+    @Option(name = "-9", usage = "Highest and slowest compression level. You "
+        + "can specify any number from 1 to 9 to adjust compression speed and "
+        + "ratio.")
+    private boolean level9;
+
+    @Argument(index = 0, required = true, usage = "The tree or commit to "
+        + "produce an archive for.")
+    private String treeIsh = "master";
+
+    @Argument(index = 1, multiValued = true, usage =
+        "Without an optional path parameter, all files and subdirectories of "
+        + "the current working directory are included in the archive. If one "
+        + "or more paths are specified, only these are included.")
+    private List<String> path;
+  }
+
+  @Inject
+  private GetArchive.AllowedFormats allowedFormats;
+  @Inject
+  private Provider<ReviewDb> db;
+  private Options options = new Options();
+
+  /**
+   * Read and parse arguments from input stream.
+   * This method gets the arguments from input stream, in Pkt-line format,
+   * then parses them to fill the options object.
+   */
+  protected void readArguments() throws IOException, Failure {
+    String argCmd = "argument ";
+    List<String> args = Lists.newArrayList();
+
+    // Read arguments in Pkt-Line format
+    PacketLineIn packetIn = new PacketLineIn(in);
+    for (;;) {
+      String s = packetIn.readString();
+      if (s == PacketLineIn.END) {
+        break;
+      }
+      if (!s.startsWith(argCmd)) {
+        throw new Failure(1, "fatal: 'argument' token or flush expected");
+      }
+      String[] parts = s.substring(argCmd.length()).split("=", 2);
+      for(String p : parts) {
+        args.add(p);
+      }
+    }
+
+    try {
+      // Parse them into the 'options' field
+      CmdLineParser parser = new CmdLineParser(options);
+      parser.parseArgument(args);
+      if (options.path == null || Arrays.asList(".").equals(options.path)) {
+        options.path = Collections.emptyList();
+      }
+    } catch (CmdLineException e) {
+      throw new Failure(2, "fatal: unable to parse arguments, " + e);
+    }
+  }
+
+  @Override
+  protected void runImpl() throws IOException, Failure {
+    PacketLineOut packetOut = new PacketLineOut(out);
+    packetOut.setFlushOnEnd(true);
+    packetOut.writeString("ACK");
+    packetOut.end();
+
+    try {
+      // Parse Git arguments
+      readArguments();
+
+      ArchiveFormat f = allowedFormats.getExtensions().get("." + options.format);
+      if (f == null) {
+        throw new Failure(3, "fatal: upload-archive not permitted");
+      }
+
+      // Find out the object to get from the specified reference and paths
+      ObjectId treeId = repo.resolve(options.treeIsh);
+      if (treeId.equals(ObjectId.zeroId())) {
+        throw new Failure(4, "fatal: reference not found");
+      }
+
+      // Verify the user has permissions to read the specified reference
+      if (!projectControl.allRefsAreVisible() && !canRead(treeId)) {
+          throw new Failure(5, "fatal: cannot perform upload-archive operation");
+      }
+
+      try {
+        // The archive is sent in DATA sideband channel
+        SideBandOutputStream sidebandOut =
+            new SideBandOutputStream(SideBandOutputStream.CH_DATA,
+                SideBandOutputStream.MAX_BUF, out);
+        new ArchiveCommand(repo)
+            .setFormat(f.name())
+            .setFormatOptions(getFormatOptions(f))
+            .setTree(treeId)
+            .setPaths(options.path.toArray(new String[0]))
+            .setPrefix(options.prefix)
+            .setOutputStream(sidebandOut)
+            .call();
+        sidebandOut.flush();
+        sidebandOut.close();
+      } catch (GitAPIException e) {
+        throw new Failure(7, "fatal: git api exception, " + e);
+      }
+    } catch (Failure f) {
+      // Report the error in ERROR sideband channel
+      SideBandOutputStream sidebandError =
+          new SideBandOutputStream(SideBandOutputStream.CH_ERROR,
+              SideBandOutputStream.MAX_BUF, out);
+      sidebandError.write(f.getMessage().getBytes(UTF_8));
+      sidebandError.flush();
+      sidebandError.close();
+      throw f;
+    } finally {
+      // In any case, cleanly close the packetOut channel
+      packetOut.end();
+    }
+  }
+
+  private Map<String, Object> getFormatOptions(ArchiveFormat f) {
+    if (f == ArchiveFormat.ZIP) {
+      int value = Arrays.asList(options.level0, options.level1, options.level2,
+          options.level3, options.level4, options.level5, options.level6,
+          options.level7, options.level8, options.level9).indexOf(true);
+      if (value >= 0) {
+        return ImmutableMap.<String, Object> of(
+            "level", Integer.valueOf(value));
+      }
+    }
+    return Collections.emptyMap();
+  }
+
+  private boolean canRead(ObjectId revId) throws IOException {
+    try (RevWalk rw = new RevWalk(repo)) {
+      RevCommit commit = rw.parseCommit(revId);
+      return projectControl.canReadCommit(db.get(), rw, commit);
+    }
+  }
+}
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index ad242eb..3bc8b58 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.contact.ContactStoreModule;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
+import com.google.gerrit.server.git.ChangeCacheImplModule;
 import com.google.gerrit.server.git.GarbageCollectionModule;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
@@ -210,6 +211,7 @@
   private Injector createDbInjector() {
     final List<Module> modules = new ArrayList<>();
     AbstractModule secureStore = createSecureStoreModule();
+    modules.add(secureStore);
     if (sitePath != null) {
       Module sitePathModule = new AbstractModule() {
         @Override
@@ -243,7 +245,6 @@
       });
 
     } else {
-      modules.add(secureStore);
       modules.add(new LifecycleModule() {
         @Override
         protected void configure() {
@@ -288,6 +289,7 @@
     modules.add(new DiffExecutorModule());
     modules.add(new MimeUtil2Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+    modules.add(new ChangeCacheImplModule(false));
     modules.add(new InternalAccountDirectory.Module());
     modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
diff --git a/lib/BUCK b/lib/BUCK
index 2760acc..d91ea26 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -188,8 +188,8 @@
 
 maven_jar(
   name = 'truth',
-  id = 'com.google.truth:truth:0.25',
-  sha1 = '503ba892e8482976b81eb2b2df292858fbac3782',
+  id = 'com.google.truth:truth:0.26',
+  sha1 = 'b5802815625d82f39c33219299771f3d64301b06',
   license = 'DO_NOT_DISTRIBUTE',
   deps = [
     ':guava',
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index fe249fa..ca989c7 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -23,7 +23,6 @@
   sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d',
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
-  visibility = ['//lib/jgit:jgit-archive'],
 )
 
 maven_jar(
@@ -41,8 +40,8 @@
 
 maven_jar(
   name = 'lang',
-  id = 'commons-lang:commons-lang:2.5',
-  sha1 = 'b0236b252e86419eef20c31a44579d2aee2f0a69',
+  id = 'commons-lang:commons-lang:2.6',
+  sha1 = '0ce1edb914c94ebc388f086c6827e8bdeec71ac2',
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
 )
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index acb96fb..9dc2e73 100644
--- a/lib/jgit/BUCK
+++ b/lib/jgit/BUCK
@@ -1,13 +1,13 @@
 include_defs('//lib/maven.defs')
 
-REPO = GERRIT  # Leave here even if set to MAVEN_CENTRAL.
-VERS = '3.7.0.201502260915-r.63-gf1a15f7'
+REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
+VERS = '4.0.0.201505191015-rc1.19-g1773002'
 
 maven_jar(
   name = 'jgit',
   id = 'org.eclipse.jgit:org.eclipse.jgit:' + VERS,
-  bin_sha1 = 'f0690b06d4270cc823b03ca88fb996897edbc410',
-  src_sha1 = 'c0b8232d4c5e8422198b35a90409c9826af68fa3',
+  bin_sha1 = '4db24b39dab8dc0e889807383728032945f461be',
+  src_sha1 = '1723a2855f50493b7c0b216aae97909a7ea59962',
   license = 'jgit',
   repository = REPO,
   unsign = True,
@@ -22,7 +22,7 @@
 maven_jar(
   name = 'jgit-servlet',
   id = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + VERS,
-  sha1 = '1284d550981a037ffe92441faf5d16cdfc09396d',
+  sha1 = '7bfdbddea56a87f3f2687ae6abf2c5bdae649f0c',
   license = 'jgit',
   repository = REPO,
   deps = [':jgit'],
@@ -36,7 +36,7 @@
 maven_jar(
   name = 'jgit-archive',
   id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
-  sha1 = '6d96c9c27cb85c6db165b160fb0e8038b66b764a',
+  sha1 = '08fce6b89f6d1e78f99869d542d70899f3be9c9f',
   license = 'jgit',
   repository = REPO,
   deps = [':jgit',
@@ -53,7 +53,7 @@
 maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
-  sha1 = '48e25624fda973f2e9b7fa70a5204ee7a72e1bb3',
+  sha1 = 'a54c16076e6cbdb9113565a82cffa5f268ae8e3b',
   license = 'DO_NOT_DISTRIBUTE',
   repository = REPO,
   unsign = True,
diff --git a/lib/log/BUCK b/lib/log/BUCK
index cadc7e7..b332f20 100644
--- a/lib/log/BUCK
+++ b/lib/log/BUCK
@@ -31,3 +31,18 @@
   license = 'Apache2.0',
   exclude = ['META-INF/LICENSE', 'META-INF/NOTICE'],
 )
+
+maven_jar(
+  name = 'jsonevent-layout',
+  id = 'net.logstash.log4j:jsonevent-layout:1.7',
+  sha1 = '507713504f0ddb75ba512f62763519c43cf46fde',
+  license = 'Apache2.0',
+  deps = [':json-smart', '//lib/commons:lang']
+)
+
+maven_jar(
+  name = 'json-smart',
+  id = 'net.minidev:json-smart:1.1.1',
+  sha1 = '24a2f903d25e004de30ac602c5b47f2d4e420a59',
+  license = 'Apache2.0',
+)
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index 275f0bb..68d579d 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -1,11 +1,11 @@
 include_defs('//lib/maven.defs')
 
-VERSION = '5.0.0'
+VERSION = '5.1.0'
 
 maven_jar(
   name = 'core',
   id = 'org.apache.lucene:lucene-core:' + VERSION,
-  sha1 = '4395e5ea987af804c4a9b96131e2ee75db061fdf',
+  sha1 = '93e64c67106f9a50e6ea01cfcfd6ac692ab3a41a',
   license = 'Apache2.0',
   exclude = [
     'META-INF/LICENSE.txt',
@@ -16,7 +16,7 @@
 maven_jar(
   name = 'analyzers-common',
   id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
-  sha1 = '6159cbc5c9631ef75e1f0e97b358ecdd8f1447a9',
+  sha1 = '54770d9b792536dff25ae1d70cd8af822c0079a3',
   license = 'Apache2.0',
   deps = [':core'],
   exclude = [
@@ -28,7 +28,7 @@
 maven_jar(
   name = 'backward-codecs',
   id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
-  sha1 = '5cd11fc1be436ff96b63f0f76f299a9d25543b0b',
+  sha1 = '5f0c5bb10ac3facace6b314bb02a6b572795b3c9',
   license = 'Apache2.0',
   deps = [':core'],
   exclude = [
@@ -40,7 +40,7 @@
 maven_jar(
   name = 'misc',
   id = 'org.apache.lucene:lucene-misc:' + VERSION,
-  sha1 = '06bd7cb030e598da81a8228f5c58630e5ce7b84a',
+  sha1 = '3b700fa57f5d444da0e58cc1855042e6c5a18640',
   license = 'Apache2.0',
   deps = [':core'],
   exclude = [
@@ -52,7 +52,7 @@
 maven_jar(
   name = 'query-parser',
   id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
-  sha1 = 'f459326c0b58bb837612bfeb37f6015c1a8962db',
+  sha1 = '53f0b3f0e700a8ec484195d3370688171e830634',
   license = 'Apache2.0',
   deps = [':core'],
   exclude = [
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
index ecec6df..8d295ed 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit ecec6dfe7ed67431d96acf9dabac3885463900e8
+Subproject commit 8d295ed48e8f52eef5661b6eb10d6402d197c776
diff --git a/plugins/download-commands b/plugins/download-commands
index baa09c2..1cf6921 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit baa09c2e265a2b264a5fb4571e7eefda04def0c4
+Subproject commit 1cf69212a7489e88d8c73377f0f77f8a5965db75
diff --git a/plugins/replication b/plugins/replication
index 7cf2973..f03316c 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 7cf2973c07b9568b4724ae6480db50e8e636bc19
+Subproject commit f03316c26b5991cba96280de59a4119619a18a58
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 4d1a7b6..b5c6f81 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 4d1a7b61034d52859cc7349af41d1068e954556d
+Subproject commit b5c6f81d979e78fbd734d946b18270ec8319eaf6