Merge "Remove unused constructor"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index f0ab339..f10623f 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -842,7 +842,7 @@
 ----
 [commentlink "changeid"]
   match = (I[0-9a-f]{8,40})
-  link = "#q,$1,n,z"
+  link = "#q,$1"
 
 [commentlink "bugzilla"]
   match = "(bug\\s+#?)(\\d+)"
@@ -1889,7 +1889,8 @@
 +
 * `SOLR`
 +
-A link:http://lucene.apache.org/solr/[Solr] index is used.
+A link:https://cwiki.apache.org/confluence/display/solr/SolrCloud[
+SolrCloud] index is used.
 
 +
 By default, `LUCENE`.
@@ -1901,6 +1902,12 @@
 Open and closed changes are indexed in separate indexes named
 'changes_open' and 'changes_closed' respectively.
 
+[[index.url]]index.url::
++
+Only used when the type is `SOLR`.
++
+URL of the index server.
+
 [[index.name.ramBufferSize]]index.name.ramBufferSize::
 +
 Only used when the type is `LUCENE`.
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index e86cdae..2fb256f 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -129,6 +129,21 @@
 +
 Common unit suffixes of k, m, or g are supported.
 
+[[receive.checkReceivedObjects]]receive.checkReceivedObjects::
++
+Controls whether or not the JGit functionality for checking received objects
+is enabled.
++
+By default Gerrit checks the validity of git objects. Setting this variable to
+false should not be used unless a project with history containing invalid
+objects needs to be pushed into a Gerrit repository.
++
+This functionality is provided as some other git implementations have allowed
+bad history to be written into git repositories. If these repositories need pushing
+up to Gerrit then the JGit checks need to be disabled.
++
+The default value for this is true, false disables the checks.
+
 [[submit-section]]
 === Submit section
 
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index b6c1411..b48671e 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -322,6 +322,47 @@
  )
 ----
 
+== Building against unpublished JARs, that change frequently
+
+If a dependent Gerrit library is undergoing active development it must be
+recompiled and the change must be reflected in the Buck build process. For
+example testing Gerrit against changed JGit snapshot version. After building
+JGit library, the artifacts are created in local Maven build directory, e. g.:
+
+----
+  mvn package
+  /home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT.jar
+  /home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT-sources.jar
+----
+
+If as usual, installation of the build artifacts takes place in local maven
+repository, then the Buck build must fetch them from there with normal
+`download_file.py` process. Disadvantage of this approach is that Buck cache
+invalidation must occur to refresh the artifacts after next
+change-compile-install round trip.
+
+To shorten that workflow and take the installation of the artifacts to the
+local Maven repository and fetching it again from there out of the picture,
+`local_jar()` method is used instead of `maven_jar()`:
+
+[source,python]
+----
+ local_jar(
+   name = 'jgit',
+   jar = '/home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT.jar',
+   src = '/home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT-sources.jar',
+   deps = [':ewah']
+ )
+----
+
+This creates a symlink to the Buck targets direct against artifacts in
+another project's Maven target directory:
+
+----
+  buck-out/gen/lib/jgit/jgit.jar ->
+  /home/<user>/projects/jgit/org.eclipse.jgit/target/org.eclipse.jgit-3.3.0-SNAPSHOT.jar
+----
+
 == Building against artifacts from custom Maven repositories
 
 To build against custom Maven repositories, two modes of operations are
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index d16734f..5e307f9 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -6,7 +6,15 @@
 
 * https://gerrit-review.googlesource.com/
 
-The Contributor License Agreements:
+A Contributor License Agreement must be completed before contributions
+are accepted.  To view and accept the agreements do the following:
+
+* Click "Sign In" at the top right corner of https://gerrit-review.googlesource.com/
+* Sign In with your Google account
+* After signing in, go to https://gerrit-review.googlesource.com/#/settings/agreements
+* Click "New Contributor Agreement" and follow the instructions
+
+For reference, the actual agreements are linked below
 
 * https://gerrit-review.googlesource.com/static/cla_individual.html
 * https://gerrit-review.googlesource.com/static/cla_corporate.html
@@ -19,7 +27,7 @@
 your change.  You can view the pending Gerrit contributions and
 their statuses here:
 
-* https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit,n,z
+* https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit
 
 Depending on the size of that list it might take a while for
 your change to get reviewed.  Naturally there are fewer
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 14470b0..e0824ca 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -437,9 +437,10 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 
-@CommandMetaData(name="print", descr="Print hello command")
+@CommandMetaData(name="print", description="Print hello command")
 class PrintHello extends SshCommand {
-  protected abstract void run() {
+  @Override
+  protected void run() {
     stdout.print("Hello\n");
   }
 }
@@ -455,7 +456,8 @@
 
 @Export("print")
 class PrintHello extends SshCommand {
-  protected abstract void run() {
+  @Override
+  protected void run() {
     stdout.print("Hello\n");
   }
 }
@@ -470,6 +472,7 @@
 import com.google.gerrit.sshd.PluginCommandModule;
 
 class MyCommands extends PluginCommandModule {
+  @Override
   protected void configureCommands() {
     command(PrintHello.class);
   }
@@ -1602,6 +1605,13 @@
 }
 ----
 
+[[settings-screen]]
+== Plugin Settings Screen
+
+If a plugin implements a screen for administrating its settings that is
+available under "#/x/<plugin-name>/settings" it is automatically linked
+from the plugin list screen.
+
 [[http]]
 == HTTP Servlets
 
diff --git a/Documentation/intro-change-screen.txt b/Documentation/intro-change-screen.txt
index 0aa6fd5..8bf9e27 100644
--- a/Documentation/intro-change-screen.txt
+++ b/Documentation/intro-change-screen.txt
@@ -28,8 +28,10 @@
 
 The new change screen is activated by default. It can be deactivated
 system-wide by changing the link:config-gerrit.html[gerrit.changeScreen]
-setting to `OLD_UI`.  Users can deactivate it by setting `OLD_UI` on their
-user preferences page.
+setting to `OLD_UI`.
+
+Users can switch between the old and new screens by selecting "Old Screen" or
+"New Screen" in the "Change View" setting on their user preferences page.
 
 [[switching-between-patch-sets]]
 == Switching between patch sets
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 0c6abb8..b28cbdb 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -64,6 +64,20 @@
 * mon, month, months (`1 month` is treated as `30 days`)
 * y, year, years (`1 year` is treated as `365 days`)
 
+[[before_until]]
+before:'TIME'/until:'TIME'
++
+Changes modified before the given 'TIME', inclusive. With no time,
+assumes 00:00:00 in the local time zone. Supports many formats supported
+by `git log`.
+
+[[after_since]]
+after:'TIME'/since:'TIME'
++
+Changes modified before the given 'TIME', inclusive. With no time,
+assumes 00:00:00 in the local time zone. Supports many formats supported
+by `git log`.
+
 [[change]]
 change:'ID'::
 +
@@ -455,21 +469,6 @@
 preferences.  Including it in a web query may lead to unpredictable
 results with regards to pagination.
 
-resume_sortkey:'KEY'::
-+
-Positions the low level scan routine to start from 'KEY' and
-continue through changes from this point.  This is most often used
-for paginating result sets.  Including this in a web query may lead
-to unpredictable results.
-
-sortkey_after:'KEY', sortkey_before:'KEY'::
-+
-Restart the low level scan routine from 'KEY'.  This is automatically
-set by the pagination system as the user navigates through results
-of a query.  Including either value in a web query may lead to
-unpredictable results.
-
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/ReleaseNotes/ReleaseNotes-2.8.2.txt b/ReleaseNotes/ReleaseNotes-2.8.2.txt
index 6d0f995..0307b59 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.2.txt
@@ -36,6 +36,23 @@
 If HEAD was detached the `GetHead` REST endpoint refused to resolve HEAD
 when the user was not a project owner.
 
+* link:https://code.google.com/p/gerrit/issues/detail?id=2932[Issue 2932]:
+Keep `status:closed` limit below MySQL Connector/J's hard limit.
++
+Since MySQL Connector/J 5.1.21 does not allow limits above 50M rows
+and aborts them with 'setMaxRows() out of range', we cannot use `MAX_VALUE`
+as limit for plain `status:closed` queries.
+
+* Fix IllegalArgumentException when running query with `limit:0` on secondary
+index.
++
+Running a query with `limit:0` when the secondary index is enabled was causing
+an internal server error.
+
+* Remove dependency on joda time library in gerrit launcher.
++
+The joda time library was being unnecessarily packaged in the root of
+the gerrit.war file.
 
 Change Screen / Diff Screen
 ---------------------------
@@ -65,6 +82,40 @@
 +
 The action was using the cherry-pick button instead of the revert button.
 
+* Improve the error message shown when cherry picking a change fails.
++
+The error message "Could not create merge commit during cherry pick" was
+confusing for users, and is replaced with simply "Cherry pick failed".
+
+* Add newline on commit messages created by cherry picking a change in the UI
+or via the REST API.
++
+If a commit was cherry-picked from the UI or via the REST API, the
+trailing newline on the end of the commit message was lost.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2405[Issue 2405]:
+Update change to invalidate cache after deletion of draft revision.
++
+When a non-current draft patch set was deleted no update of the change
+was made, causing the change screen to not work properly because it
+relied on cached data.
+
+* Extend change screen's horizontal bars to full width.
++
+This allows the title of the change message to have some padding within
+the bar.
+
+* Fix tab alignment to be correct width in side-by-side diff.
++
+This fixes the tab width to be the user's preference, rather than
+1 + user's preference when show tabs is enabled.
+
+* Fill the browser width in side-by-side diff.
++
+Filling the browser available space with each side of the diff at
+50% size allows the user to more easily view long lines if they
+have a wide display, and better fit on more narrow displays by
+splitting the available width at 50%.
 
 ssh
 ---
@@ -93,6 +144,8 @@
 The `replication` command is provided by the replication plugin, so it is no
 longer relevant to mention this in the description of a core command.
 
+* Fix aliasing of SSH commands.
+
 
 Replication Plugin
 ------------------
@@ -118,6 +171,11 @@
 * Update documentation to clarify replication of refs/meta/config when
 refspec is 'all refs'.
 
+Upgrades
+--------
+
+
+* JGit is upgraded to 3.2.0.201312181205-r
 
 Documentation
 -------------
diff --git a/contrib/trivial_rebase.py b/contrib/trivial_rebase.py
deleted file mode 100755
index c97172e..0000000
--- a/contrib/trivial_rebase.py
+++ /dev/null
@@ -1,253 +0,0 @@
-#!/usr/bin/env python
-
-# Copyright (c) 2010, Code Aurora Forum. All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are
-# met:
-#    # Redistributions of source code must retain the above copyright
-#       notice, this list of conditions and the following disclaimer.
-#    # Redistributions in binary form must reproduce the above
-#       copyright notice, this list of conditions and the following
-#       disclaimer in the documentation and/or other materials provided
-#       with the distribution.
-#    # Neither the name of Code Aurora Forum, Inc. nor the names of its
-#       contributors may be used to endorse or promote products derived
-#       from this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
-# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
-# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
-# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
-# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
-# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
-# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
-# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-""" This script is designed to detect when a patchset uploaded to Gerrit is
-'identical' (determined via git-patch-id) and reapply reviews onto the new
-patchset from the previous patchset.
-
-Get usage and help info by running: ./trivial_rebase.py --help
-Documentation is available here: https://www.codeaurora.org/xwiki/bin/QAEP/Gerrit
-
-"""
-
-from __future__ import print_function
-
-import argparse
-import json
-import re
-import subprocess
-import sys
-
-class TrivialRebase:
-  def __init__(self):
-    usage = "%(prog)s <required options> [--server-port=PORT]"
-    parser = argparse.ArgumentParser(usage=usage)
-    parser.add_argument("--change-url", dest="changeUrl", help="Change URL")
-    parser.add_argument("--project", help="Project path in Gerrit")
-    parser.add_argument("--commit", help="Git commit-ish for this patchset")
-    parser.add_argument("--patchset", type=int, help="The patchset number")
-    parser.add_argument("--private-key-path", dest="private_key_path",
-                        help="Full path to Gerrit SSH daemon's private host key")
-    parser.add_argument("--server", default='localhost',
-                        help="Gerrit SSH server [default: %(default)s]")
-    parser.add_argument("--server-port", dest="port", default='29418',
-                        help="Port to connect to Gerrit's SSH daemon "
-                             "[default: %(default)s]")
-    parser.add_argument("--ssh", default="ssh", help="SSH executable")
-    parser.add_argument("--ssh-port-flag", dest="ssh_port_flag", default="-p", help="SSH port flag")
-
-    args = parser.parse_known_args()[0]
-    if None in [args.changeUrl, args.project, args.commit, args.patchset]:
-      parser.error("Incomplete arguments")
-    try:
-      self.changeId = re.search(r'\d+$', args.changeUrl).group()
-    except AttributeError:
-      parser.error("Invalid changeId")
-    self.project = args.project
-    self.commit = args.commit
-    self.patchset = args.patchset
-    self.private_key_path = args.private_key_path
-    self.server = args.server
-    self.port = args.port
-    self.ssh = args.ssh
-    self.ssh_port_flag = args.ssh_port_flag
-
-  class CheckCallError(OSError):
-    """CheckCall() returned non-0."""
-    def __init__(self, command, cwd, retcode, stdout, stderr=None):
-      OSError.__init__(self, command, cwd, retcode, stdout, stderr)
-      self.command = command
-      self.cwd = cwd
-      self.retcode = retcode
-      self.stdout = stdout
-      self.stderr = stderr
-
-  def CheckCall(self, command, cwd=None):
-    """Like subprocess.check_call() but returns stdout.
-
-    Works on python 2.4
-
-    """
-    try:
-      process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-      std_out, std_err = process.communicate()
-    except OSError as e:
-      raise self.CheckCallError(command, cwd, e.errno, None)
-    if process.returncode:
-      raise self.CheckCallError(command, cwd, process.returncode, std_out, std_err)
-    return std_out, std_err
-
-  def GsqlQuery(self, sql_query):
-    """Run a gerrit gsql query and return the result."""
-    gsql_cmd = [self.ssh, self.ssh_port_flag, self.port, self.server, 'gerrit', 'gsql',
-                '--format', 'JSON', '-c', sql_query]
-    try:
-      (gsql_out, _gsql_stderr) = self.CheckCall(gsql_cmd)
-    except self.CheckCallError as e:
-      print("return code is %s" % e.retcode)
-      print("stdout and stderr is\n%s%s" % (e.stdout, e.stderr))
-      raise
-
-    new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
-    return new_out.split('split here\n')
-
-  def FindPrevRev(self):
-    """Find the revision of the previous patch set on the change."""
-    sql_query = ("\"SELECT revision FROM patch_sets WHERE "
-                 "change_id = %s AND patch_set_id = %s\"" %
-                 (self.changeId, (self.patchset - 1)))
-    revisions = self.GsqlQuery(sql_query)
-
-    json_dict = json.loads(revisions[0], strict=False)
-    return json_dict["columns"]["revision"]
-
-  def GetApprovals(self):
-    """Get all the approvals on a specific patch set.
-
-    Returns a list of approval dicts.
-
-    """
-    sql_query = ("\"SELECT value,account_id,category_id AS label FROM patch_set_approvals "
-                 "WHERE change_id = %s AND patch_set_id = %s AND value != 0\""
-                 % (self.changeId, (self.patchset - 1)))
-    gsql_out = self.GsqlQuery(sql_query)
-    approvals = []
-    for json_str in gsql_out:
-      data = json.loads(json_str, strict=False)
-      if data["type"] == "row":
-        approvals.append(data["columns"])
-    return approvals
-
-  def AppendAcctApproval(self, account_id, value):
-    try:
-      newval = self.acct_approvals[account_id] + ' ' + value
-    except KeyError:
-      newval = value
-    self.acct_approvals[account_id] = newval
-
-  def GetEmailFromAcctId(self, account_id):
-    """Return the preferred email address associated with the account_id."""
-    sql_query = ("\"SELECT preferred_email FROM accounts WHERE account_id = %s\""
-                 % account_id)
-    email_addr = self.GsqlQuery(sql_query)
-
-    json_dict = json.loads(email_addr[0], strict=False)
-    return json_dict["columns"]["preferred_email"]
-
-  def GetPatchId(self, revision):
-    git_show_cmd = ['git', 'show', revision]
-    patch_id_cmd = ['git', 'patch-id']
-    git_show_process = subprocess.Popen(git_show_cmd, stdout=subprocess.PIPE)
-    patch_id_process = subprocess.Popen(patch_id_cmd, stdout=subprocess.PIPE,
-                                        stdin=git_show_process.stdout)
-    res = patch_id_process.communicate()[0] or '0'
-    return res.split()[0]
-
-  def SuExec(self, as_user, cmd):
-    suexec_cmd = [self.ssh, '-l', "Gerrit Code Review", self.ssh_port_flag, self.port, self.server]
-    if self.private_key_path:
-      suexec_cmd += ['-i', self.private_key_path]
-    suexec_cmd += ['suexec', '--as', as_user, '--', cmd]
-    self.CheckCall(suexec_cmd)
-
-  def DiffCommitMessages(self, prev_commit):
-    log_cmd1 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
-                prev_commit + '^!']
-    commit1_log = self.CheckCall(log_cmd1)
-    log_cmd2 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
-                self.commit + '^!']
-    commit2_log = self.CheckCall(log_cmd2)
-    if commit1_log != commit2_log:
-      return True
-    return False
-
-  def Run(self):
-    if self.patchset == 1:
-      # Nothing to detect on first patchset
-      return
-    prev_revision = self.FindPrevRev()
-    assert prev_revision, "Previous revision not found"
-    prev_patch_id = self.GetPatchId(prev_revision)
-    cur_patch_id = self.GetPatchId(self.commit)
-    if prev_patch_id == '0' and cur_patch_id == '0':
-      print("commits %s and %s are both empty or merge commits" % (prev_revision, self.commit))
-      return
-    if cur_patch_id != prev_patch_id:
-      # patch-ids don't match
-      return
-    # Patch ids match. This is a trivial rebase.
-    # In addition to patch-id we should check if the commit message changed. Most
-    # approvers would want to re-review changes when the commit message changes.
-    changed = self.DiffCommitMessages(prev_revision)
-    if changed:
-      # Insert a comment into the change letting the approvers know only the
-      # commit message changed
-      comment_msg = ("\'--message=New patchset patch-id matches previous patchset"
-                     ", but commit message has changed.'")
-      comment_cmd = [self.ssh, self.ssh_port_flag, self.port, self.server, 'gerrit',
-                     'review', '--project', self.project, comment_msg, self.commit]
-      self.CheckCall(comment_cmd)
-      return
-
-    # Need to get all approvals on prior patch set, then suexec them onto
-    # this patchset.
-    approvals = self.GetApprovals()
-    self.acct_approvals = dict()
-    for approval in approvals:
-      # Note: Sites with different 'copy_min_score' values in the
-      # approval_categories DB table might want different behavior here.
-      # Additional categories should also be added if desired.
-      if approval["label"] == "Code-Review":
-        if approval['value'] != '-2':
-          self.AppendAcctApproval(approval['account_id'],
-                                  '--label Code-Review=%s' % approval['value'])
-      elif approval["label"] == "Verified":
-        # Don't re-add verifies
-        # self.AppendAcctApproval(approval['account_id'], '--label Verified=%s' % approval['value'])
-        continue
-      elif approval["label"] == "SUBM":
-        # We don't care about previous submit attempts
-        continue
-      else:
-        self.AppendAcctApproval(approval['account_id'], '--label %s=%s' %
-                                (approval['label'], approval['value']))
-
-    gerrit_review_msg = ("\'Automatically re-added by Gerrit trivial rebase "
-                          "detection script.\'")
-    for acct, flags in list(self.acct_approvals.items()):
-      gerrit_review_cmd = ['gerrit', 'review', '--project', self.project,
-                            '--message', gerrit_review_msg, flags, self.commit]
-      email_addr = self.GetEmailFromAcctId(acct)
-      self.SuExec(email_addr, ' '.join(gerrit_review_cmd))
-
-if __name__ == "__main__":
-  try:
-    TrivialRebase().Run()
-  except AssertionError as e:
-    print(e, file=sys.stderr)
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 66e00b7..14571cd 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
@@ -14,18 +14,31 @@
 
 package com.google.gerrit.acceptance;
 
+import static com.google.gerrit.acceptance.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.GitUtil.initSsh;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.base.Joiner;
+import com.google.common.primitives.Chars;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ListChangesOption;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gson.Gson;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
+import com.google.inject.util.Providers;
 
 import org.apache.http.HttpStatus;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Rule;
 import org.junit.rules.TestRule;
@@ -34,6 +47,10 @@
 import org.junit.runners.model.Statement;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
 
 @RunWith(ConfigSuite.class)
 public abstract class AbstractDaemonTest {
@@ -43,9 +60,30 @@
   @Inject
   protected AccountCreator accounts;
 
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  protected GerritApi gApi;
+
+  @Inject
+  private AcceptanceTestRequestScope atrScope;
+
+  @Inject
+  private IdentifiedUser.GenericFactory identifiedUserFactory;
+
+  @Inject
+  protected PushOneCommit.Factory pushFactory;
+
+  protected Git git;
   protected GerritServer server;
   protected TestAccount admin;
+  protected TestAccount user;
   protected RestSession adminSession;
+  protected RestSession userSession;
+  protected SshSession sshSession;
+  protected ReviewDb db;
+  protected Project.NameKey project;
 
   @Rule
   public TestRule testRunner = new TestRule() {
@@ -55,7 +93,9 @@
         @Override
         public void evaluate() throws Throwable {
           boolean mem = description.getAnnotation(UseLocalDisk.class) == null;
-          beforeTest(config(description), mem);
+          boolean enableHttpd = description.getAnnotation(NoHttpd.class) == null
+              && description.getTestClass().getAnnotation(NoHttpd.class) == null;
+          beforeTest(config(description), mem, enableHttpd);
           base.evaluate();
           afterTest();
         }
@@ -78,33 +118,79 @@
     }
   }
 
-  private void beforeTest(Config cfg, boolean memory) throws Exception {
-    server = startServer(cfg, memory);
+  private void beforeTest(Config cfg, boolean memory, boolean enableHttpd) throws Exception {
+    server = startServer(cfg, memory, enableHttpd);
     server.getTestInjector().injectMembers(this);
     admin = accounts.admin();
+    user = accounts.user();
     adminSession = new RestSession(server, admin);
+    userSession = new RestSession(server, user);
     initSsh(admin);
+    db = reviewDbProvider.open();
+    atrScope.set(atrScope.newContext(reviewDbProvider, sshSession,
+        identifiedUserFactory.create(Providers.of(db), admin.getId())));
+    sshSession = new SshSession(server, admin);
+    project = new Project.NameKey("p");
+    createProject(sshSession, project.get());
+    git = cloneProject(sshSession.getUrl() + "/" + project.get());
   }
 
-  protected GerritServer startServer(Config cfg, boolean memory) throws Exception {
-    return GerritServer.start(cfg, memory);
+  protected GerritServer startServer(Config cfg, boolean memory,
+      boolean enableHttpd) throws Exception {
+    return GerritServer.start(cfg, memory, enableHttpd);
   }
 
   private void afterTest() throws Exception {
+    db.close();
+    sshSession.close();
     server.stop();
   }
 
-  protected ChangeInfo getChange(String changeId, ListChangesOption... options)
+  protected PushOneCommit.Result createChange() throws GitAPIException,
+      IOException {
+    PushOneCommit push = pushFactory.create(db, admin.getIdent());
+    return push.to(git, "refs/for/master");
+  }
+
+  private static final List<Character> RANDOM =
+      Chars.asList(new char[]{'a','b','c','d','e','f','g','h'});
+  protected PushOneCommit.Result ammendChange(String changeId)
+      throws GitAPIException, IOException {
+    Collections.shuffle(RANDOM);
+    PushOneCommit push =
+        pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
+            PushOneCommit.FILE_NAME, new String(Chars.toArray(RANDOM)), changeId);
+    return push.to(git, "refs/for/master");
+  }
+
+  protected ChangeJson.ChangeInfo getChange(String changeId, ListChangesOption... options)
       throws IOException {
     return getChange(adminSession, changeId, options);
   }
 
-  protected ChangeInfo getChange(RestSession session, String changeId,
+  protected ChangeJson.ChangeInfo getChange(RestSession session, String changeId,
       ListChangesOption... options) throws IOException {
     String q = options.length > 0 ? "?o=" + Joiner.on("&o=").join(options) : "";
     RestResponse r = session.get("/changes/" + changeId + q);
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    return newGson().fromJson(r.getReader(), ChangeInfo.class);
+    return newGson().fromJson(r.getReader(), ChangeJson.ChangeInfo.class);
+  }
+
+  protected ChangeInfo info(String id)
+      throws RestApiException {
+    return gApi.changes().id(id).info();
+  }
+
+  protected ChangeInfo get(String id)
+      throws RestApiException {
+    return gApi.changes().id(id).get();
+  }
+
+  protected ChangeInfo get(String id, ListChangesOption... options)
+      throws RestApiException {
+    EnumSet<ListChangesOption> s = EnumSet.noneOf(ListChangesOption.class);
+    s.addAll(Arrays.asList(options));
+    return gApi.changes().id(id).get(s);
   }
 
   protected static Gson newGson() {
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 272b5ab..a8ce229 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
@@ -49,7 +49,8 @@
 public class GerritServer {
 
   /** Returns fully started Gerrit server */
-  static GerritServer start(Config cfg, boolean memory) throws Exception {
+  static GerritServer start(Config cfg, boolean memory, boolean enableHttpd)
+      throws Exception {
     final CyclicBarrier serverStarted = new CyclicBarrier(2);
     final Daemon daemon = new Daemon(new Runnable() {
       public void run() {
@@ -71,6 +72,7 @@
       cfg.setBoolean("httpd", null, "requestLog", false);
       cfg.setBoolean("sshd", null, "requestLog", false);
       cfg.setBoolean("index", "lucene", "testInmemory", true);
+      daemon.setEnableHttpd(enableHttpd);
       daemon.setLuceneModule(new LuceneIndexModule(
           ChangeSchemas.getLatest().getVersion(),
           Runtime.getRuntime().availableProcessors(), null));
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/NoHttpd.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/NoHttpd.java
new file mode 100644
index 0000000..378439c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/NoHttpd.java
@@ -0,0 +1,27 @@
+// 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.acceptance;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({TYPE, METHOD})
+@Retention(RUNTIME)
+public @interface NoHttpd {
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
index 9aed646..cc19c15 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -56,8 +56,7 @@
 
 public class PushOneCommit {
   public static final String SUBJECT = "test commit";
-
-  private static final String FILE_NAME = "a.txt";
+  static final String FILE_NAME = "a.txt";
   private static final String FILE_CONTENT = "some content";
 
   public interface Factory {
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 ec0f72e..6dff20a 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
@@ -14,87 +14,32 @@
 
 package com.google.gerrit.acceptance.api.change;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeStatus;
-import com.google.gerrit.extensions.common.ListChangesOption;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.util.Providers;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
-import java.util.EnumSet;
 
+@NoHttpd
 public class ChangeIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private GerritApi gApi;
-
-  @Inject
-  private AcceptanceTestRequestScope atrScope;
-
-  @Inject
-  private IdentifiedUser.GenericFactory identifiedUserFactory;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private TestAccount user;
-
-  private Git git;
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    user = accounts.user();
-    Project.NameKey project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    db = reviewDbProvider.open();
-    atrScope.set(atrScope.newContext(reviewDbProvider, sshSession,
-        identifiedUserFactory.create(Providers.of(db), admin.getId())));
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void get() throws GitAPIException,
       IOException, RestApiException {
     PushOneCommit.Result r = createChange();
     String triplet = "p~master~" + r.getChangeId();
-    ChangeInfo c =
-        gApi.changes()
-            .id(triplet)
-            .get(EnumSet.noneOf(ListChangesOption.class));
+    ChangeInfo c = info(triplet);
     assertEquals(triplet, c.id);
     assertEquals("p", c.project);
     assertEquals("master", c.branch);
@@ -164,10 +109,4 @@
         .id("p~master~" + r.getChangeId())
         .addReviewer(in);
   }
-
-  private PushOneCommit.Result createChange() throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/for/master");
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 93b275f..54afd0b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -14,60 +14,19 @@
 
 package com.google.gerrit.acceptance.api.project;
 
-import static com.google.gerrit.acceptance.GitUtil.createProject;
-
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
 
+@NoHttpd
 public class ProjectIT extends AbstractDaemonTest  {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private GerritApi gApi;
-
-  @Inject
-  private AcceptanceTestRequestScope atrScope;
-
-  @Inject
-  private IdentifiedUser.GenericFactory identifiedUserFactory;
-
-  private ReviewDb db;
-  Project.NameKey project;
-
-  @Before
-  public void setUp() throws Exception {
-    project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    db = reviewDbProvider.open();
-    atrScope.set(atrScope.newContext(reviewDbProvider, sshSession,
-        identifiedUserFactory.create(Providers.of(db), admin.getId())));
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void createBranch() throws GitAPIException,
       IOException, RestApiException {
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 02959f1d..72b7ff1 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
@@ -14,71 +14,23 @@
 
 package com.google.gerrit.acceptance.api.revision;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
-
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.api.changes.CherryPickInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.util.Providers;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
 
+@NoHttpd
 public class RevisionIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private GerritApi gApi;
-
-  @Inject
-  private AcceptanceTestRequestScope atrScope;
-
-  @Inject
-  private IdentifiedUser.GenericFactory identifiedUserFactory;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Git git;
-  private ReviewDb db;
-  private Project.NameKey project;
-
-  @Before
-  public void setUp() throws Exception {
-    project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    db = reviewDbProvider.open();
-    atrScope.set(atrScope.newContext(reviewDbProvider, sshSession,
-        identifiedUserFactory.create(Providers.of(db), admin.getId())));
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void reviewTriplet() throws GitAPIException,
       IOException, RestApiException {
@@ -160,13 +112,6 @@
         .submit();
   }
 
-  private PushOneCommit.Result createChange() throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
-        "test commit", "a.txt", "some content");
-    return push.to(git, "refs/for/master");
-  }
-
   private PushOneCommit.Result updateChange(PushOneCommit.Result r,
       String content) throws GitAPIException, IOException {
     PushOneCommit push = pushFactory.create(db, admin.getIdent(),
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 203735f..5a8ba0c 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
@@ -15,24 +15,16 @@
 package com.google.gerrit.acceptance.git;
 
 import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
 
 import com.jcraft.jsch.JSchException;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -43,26 +35,11 @@
     SSH, HTTP
   }
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  protected PushOneCommit.Factory pushFactory;
-
-  private Project.NameKey project;
-  private Git git;
-  private ReviewDb db;
   private String sshUrl;
 
   @Before
   public void setUp() throws Exception {
-    project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
     sshUrl = sshSession.getUrl();
-    sshSession.close();
-
-    db = reviewDbProvider.open();
   }
 
   protected void selectProtocol(Protocol p) throws GitAPIException, IOException {
@@ -80,11 +57,6 @@
     git = cloneProject(url + "/" + project.get());
   }
 
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void testPushForMaster() throws GitAPIException, OrmException,
       IOException {
@@ -112,7 +84,6 @@
   public void testPushForMasterWithCc() throws GitAPIException, OrmException,
       IOException, JSchException {
     // cc one user
-    TestAccount user = accounts.create("user", "user@example.com", "User");
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email);
     r.assertOkStatus();
@@ -137,7 +108,6 @@
   public void testPushForMasterWithReviewer() throws GitAPIException,
       OrmException, IOException, JSchException {
     // add one reviewer
-    TestAccount user = accounts.create("user", "user@example.com", "User");
     String topic = "my/topic";
     PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%r=" + user.email);
     r.assertOkStatus();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
index a428ee3..b69c264 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
@@ -14,38 +14,30 @@
 
 package com.google.gerrit.acceptance.git;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
 import static com.google.gerrit.server.project.Util.grant;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
 
+@NoHttpd
 public class DraftChangeBlockedIT extends AbstractDaemonTest {
 
   @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
   private ProjectCache projectCache;
 
   @Inject
@@ -54,27 +46,13 @@
   @Inject
   private MetaDataUpdate.Server metaDataUpdateFactory;
 
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Project.NameKey project;
-  private Git git;
-  private ReviewDb db;
-
   @Before
   public void setUp() throws Exception {
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
     grant(cfg, Permission.PUSH, ANONYMOUS_USERS,
         "refs/drafts/*").setBlock();
     saveProjectConfig(cfg);
-
-    project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-
-    db = reviewDbProvider.open();
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    sshSession.close();
+    projectCache.evict(cfg.getProject());
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
index 5251d2d..c037e76 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SshPushForReviewIT.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.acceptance.git;
 
+import com.google.gerrit.acceptance.NoHttpd;
+
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.junit.Before;
 
 import java.io.IOException;
 
+@NoHttpd
 public class SshPushForReviewIT extends AbstractPushForReview {
   @Before
   public void selectSshUrl() throws GitAPIException, IOException {
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 f350909..a73169d 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
@@ -22,6 +22,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.common.data.AccessSection;
@@ -62,6 +63,7 @@
 
 import java.io.IOException;
 
+@NoHttpd
 public class SubmitOnPushIT extends AbstractDaemonTest {
 
   @Inject
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
index 4175550..9d22ee4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CapabilitiesIT.java
@@ -21,7 +21,6 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
@@ -36,7 +35,6 @@
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -52,13 +50,6 @@
   @Inject
   private ProjectCache projectCache;
 
-  private RestSession userSession;
-
-  @Before
-  public void setUp() throws Exception {
-    userSession = new RestSession(server, accounts.user());
-  }
-
   @Test
   public void testCapabilitiesUser() throws IOException,
       ConfigInvalidException, IllegalArgumentException,
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
index 605dd21..9470df0 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/StarredChangesIT.java
@@ -14,58 +14,23 @@
 
 package com.google.gerrit.acceptance.rest.account;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
 
 public class StarredChangesIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Git git;
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    Project.NameKey project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    sshSession.close();
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void starredChangeState() throws GitAPIException, IOException,
       OrmException {
@@ -93,9 +58,4 @@
       assertEquals(204, r.getStatusCode());
     }
   }
-
-  private Result createChange() throws GitAPIException, IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/for/master");
-  }
 }
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 1ec6a8e..797a20e 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
@@ -36,7 +36,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gerrit.server.change.ChangeJson.LabelInfo;
@@ -45,7 +44,6 @@
 import com.google.gerrit.server.project.PutConfig;
 import com.google.gson.reflect.TypeToken;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
 import com.jcraft.jsch.JSchException;
@@ -68,9 +66,6 @@
 public abstract class AbstractSubmit extends AbstractDaemonTest {
 
   @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
   private GitRepositoryManager repoManager;
 
   @Inject
@@ -79,16 +74,10 @@
   @Inject
   private ApprovalsUtil approvalsUtil;
 
-  @Inject
-  protected PushOneCommit.Factory pushFactory;
-
-  private Project.NameKey project;
-  private ReviewDb db;
 
   @Before
   public void setUp() throws Exception {
-    project = new Project.NameKey("p");
-    db = reviewDbProvider.open();
+    project = new Project.NameKey("p2");
   }
 
   @After
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 fff1d1d..6352b50 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
@@ -14,106 +14,50 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
-import static com.google.gerrit.extensions.common.ListChangesOption.MESSAGES;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.extensions.common.ListChangesOption;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.util.Providers;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
-import java.util.EnumSet;
 import java.util.Iterator;
 
 public class ChangeMessagesIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private GerritApi gApi;
-
-  @Inject
-  private AcceptanceTestRequestScope atrScope;
-
-  @Inject
-  private IdentifiedUser.GenericFactory identifiedUserFactory;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Git git;
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    Project.NameKey project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    sshSession.close();
-    db = reviewDbProvider.open();
-    atrScope.set(atrScope.newContext(reviewDbProvider, sshSession,
-        identifiedUserFactory.create(Providers.of(db), admin.getId())));
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
-  public void messagesNotReturnedByDefault() throws GitAPIException,
-      IOException, RestApiException {
-    String changeId = createChange();
+  public void messagesNotReturnedByDefault() throws Exception {
+    String changeId = createChange().getChangeId();
     postMessage(changeId, "Some nits need to be fixed.");
-    ChangeInfo c = getChange("p~master~" + changeId,
-        EnumSet.noneOf(ListChangesOption.class));
+    ChangeInfo c = info(changeId);
     assertNull(c.messages);
   }
 
   @Test
   public void defaultMessage() throws GitAPIException, IOException,
       RestApiException {
-    String changeId = createChange();
-    ChangeInfo c = getChange("p~master~" + changeId, EnumSet.of(MESSAGES));
+    String changeId = createChange().getChangeId();
+    ChangeInfo c = get(changeId);
     assertNotNull(c.messages);
     assertEquals(1, c.messages.size());
     assertEquals("Uploaded patch set 1.", c.messages.iterator().next().message);
   }
 
   @Test
-  public void messagesReturnedInChronologicalOrder() throws GitAPIException,
-      IOException, RestApiException {
-    String changeId = createChange();
+  public void messagesReturnedInChronologicalOrder() throws Exception {
+    String changeId = createChange().getChangeId();
     String firstMessage = "Some nits need to be fixed.";
     postMessage(changeId, firstMessage);
     String secondMessage = "I like this feature.";
     postMessage(changeId, secondMessage);
-    ChangeInfo c = getChange("p~master~" + changeId, EnumSet.of(MESSAGES));
+    ChangeInfo c = get(changeId);
     assertNotNull(c.messages);
     assertEquals(3, c.messages.size());
     Iterator<ChangeMessageInfo> it = c.messages.iterator();
@@ -122,25 +66,13 @@
     assertMessage(secondMessage, it.next().message);
   }
 
-  private String createChange() throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/for/master").getChangeId();
-  }
-
   private void assertMessage(String expected, String actual) {
     assertEquals("Patch Set 1:\n\n" + expected, actual);
   }
 
-  private void postMessage(String changeId, String msg) throws IOException {
+  private void postMessage(String changeId, String msg) throws Exception {
     ReviewInput in = new ReviewInput();
     in.message = msg;
-    adminSession.post("/changes/" + changeId + "/revisions/1/review", in)
-        .consume();
-  }
-
-  private ChangeInfo getChange(String triplet, EnumSet<ListChangesOption> s)
-      throws RestApiException {
-    return gApi.changes().id(triplet).get(s);
+    gApi.changes().id(changeId).current().review(in);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index ee86e11..83efd30 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -30,91 +30,65 @@
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
-import com.jcraft.jsch.JSchException;
-
 import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
 
 public class ChangeOwnerIT extends AbstractDaemonTest {
 
   @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
   private MetaDataUpdate.Server metaDataUpdateFactory;
 
   @Inject
   private ProjectCache projectCache;
 
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private TestAccount owner;
-  private TestAccount dev;
+  private TestAccount user2;
 
   private RestSession sessionOwner;
   private RestSession sessionDev;
-  private Git git;
-  private ReviewDb db;
-  private Project.NameKey project;
 
   @Before
   public void setUp() throws Exception {
-    newProject();
-    owner = accounts.user();
-    sessionOwner = new RestSession(server, owner);
-    SshSession sshSession = new SshSession(server, owner);
-    initSsh(owner);
+    sessionOwner = new RestSession(server, user);
+    SshSession sshSession = new SshSession(server, user);
+    initSsh(user);
     // need to initialize intern session
     createProject(sshSession, "foo");
     git = cloneProject(sshSession.getUrl() + "/" + project.get());
     sshSession.close();
-    dev = accounts.user2();
-    sessionDev = new RestSession(server, dev);
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
+    user2 = accounts.user2();
+    sessionDev = new RestSession(server, user2);
   }
 
   @Test
   public void testChangeOwner_OwnerACLNotGranted() throws GitAPIException,
       IOException, OrmException, ConfigInvalidException {
-    approve(sessionOwner, createChange(), HttpStatus.SC_FORBIDDEN);
+    approve(sessionOwner, createMyChange(), HttpStatus.SC_FORBIDDEN);
   }
 
   @Test
   public void testChangeOwner_OwnerACLGranted() throws GitAPIException,
       IOException, OrmException, ConfigInvalidException {
     grantApproveToChangeOwner();
-    approve(sessionOwner, createChange(), HttpStatus.SC_OK);
+    approve(sessionOwner, createMyChange(), HttpStatus.SC_OK);
   }
 
   @Test
   public void testChangeOwner_NotOwnerACLGranted() throws GitAPIException,
       IOException, OrmException, ConfigInvalidException {
     grantApproveToChangeOwner();
-    approve(sessionDev, createChange(), HttpStatus.SC_FORBIDDEN);
+    approve(sessionDev, createMyChange(), HttpStatus.SC_FORBIDDEN);
   }
 
   private void approve(RestSession s, String changeId, int expected)
@@ -142,17 +116,9 @@
     projectCache.evict(config.getProject());
   }
 
-  private String createChange() throws GitAPIException,
+  private String createMyChange() throws GitAPIException,
       IOException {
-    PushOneCommit push = pushFactory.create(db, owner.getIdent());
+    PushOneCommit push = pushFactory.create(db, user.getIdent());
     return push.to(git, "refs/for/master").getChangeId();
   }
-
-  private void newProject() throws UnsupportedEncodingException,
-      OrmException, JSchException, IOException {
-    project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    sshSession.close();
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java
index c8d8caf..3370cb6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConflictsOperatorIT.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.acceptance.rest.change;
 
 import static com.google.gerrit.acceptance.GitUtil.checkout;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -23,23 +22,16 @@
 import com.google.common.collect.ImmutableSet;
 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.RestResponse;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
 
 import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -47,27 +39,11 @@
 
 public class ConflictsOperatorIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Project.NameKey project;
-  private ReviewDb db;
   private int count;
 
-  @Before
-  public void setUp() throws Exception {
-    project = new Project.NameKey("p");
-
-    db = reviewDbProvider.open();
-  }
-
   @Test
   public void noConflictingChanges() throws JSchException, IOException,
       GitAPIException {
-    Git git = createProject();
     PushOneCommit.Result change = createChange(git, true);
     createChange(git, false);
 
@@ -78,7 +54,6 @@
   @Test
   public void conflictingChanges() throws JSchException, IOException,
       GitAPIException {
-    Git git = createProject();
     PushOneCommit.Result change = createChange(git, true);
     PushOneCommit.Result conflictingChange1 = createChange(git, true);
     PushOneCommit.Result conflictingChange2 = createChange(git, true);
@@ -88,17 +63,6 @@
     assertChanges(changes, conflictingChange1, conflictingChange2);
   }
 
-  private Git createProject() throws JSchException, IOException,
-      GitAPIException {
-    SshSession sshSession = new SshSession(server, admin);
-    try {
-      GitUtil.createProject(sshSession, project.get(), null, true);
-      return cloneProject(sshSession.getUrl() + "/" + project.get());
-    } finally {
-      sshSession.close();
-    }
-  }
-
   private PushOneCommit.Result createChange(Git git, boolean conflicting)
       throws GitAPIException, IOException {
     checkout(git, "origin/master");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
index de7c263..3ba5ed7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftChangeIT.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.Iterables;
@@ -23,57 +21,28 @@
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeStatus;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
 
 public class DeleteDraftChangeIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Git git;
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    Project.NameKey project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    sshSession.close();
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void deleteChange() throws GitAPIException,
-      IOException {
-    String changeId = createChange();
-    ChangeInfo c = getChange(changeId);
-    assertEquals("p~master~" + changeId, c.id);
-    assertEquals(Change.Status.NEW, c.status);
+      IOException, RestApiException {
+    String changeId = createChange().getChangeId();
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertEquals(triplet, c.id);
+    assertEquals(ChangeStatus.NEW, c.status);
     RestResponse r = deleteChange(changeId, adminSession);
     assertEquals("Change is not a draft", r.getEntityContent());
     assertEquals(409, r.getStatusCode());
@@ -81,45 +50,41 @@
 
   @Test
   public void deleteDraftChange() throws GitAPIException,
-      IOException, OrmException {
+      IOException, RestApiException, OrmException {
     String changeId = createDraftChange();
-    ChangeInfo c = getChange(changeId);
-    assertEquals("p~master~" + changeId, c.id);
-    assertEquals(Change.Status.DRAFT, c.status);
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertEquals(triplet, c.id);
+    assertEquals(ChangeStatus.DRAFT, c.status);
     RestResponse r = deleteChange(changeId, adminSession);
     assertEquals(204, r.getStatusCode());
   }
 
   @Test
   public void publishDraftChange() throws GitAPIException,
-      IOException {
+      IOException, RestApiException {
     String changeId = createDraftChange();
-    ChangeInfo c = getChange(changeId);
-    assertEquals("p~master~" + changeId, c.id);
-    assertEquals(Change.Status.DRAFT, c.status);
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertEquals(triplet, c.id);
+    assertEquals(ChangeStatus.DRAFT, c.status);
     RestResponse r = publishChange(changeId);
     assertEquals(204, r.getStatusCode());
-    c = getChange(changeId);
-    assertEquals(Change.Status.NEW, c.status);
+    c = get(triplet);
+    assertEquals(ChangeStatus.NEW, c.status);
   }
 
   @Test
   public void publishDraftPatchSet() throws GitAPIException,
-      IOException, OrmException {
+      IOException, OrmException, RestApiException {
     String changeId = createDraftChange();
-    ChangeInfo c = getChange(changeId);
-    assertEquals("p~master~" + changeId, c.id);
-    assertEquals(Change.Status.DRAFT, c.status);
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertEquals(triplet, c.id);
+    assertEquals(ChangeStatus.DRAFT, c.status);
     RestResponse r = publishPatchSet(changeId);
     assertEquals(204, r.getStatusCode());
-    c = getChange(changeId);
-    assertEquals(Change.Status.NEW, c.status);
-  }
-
-  private String createChange() throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    return push.to(git, "refs/for/master").getChangeId();
+    assertEquals(ChangeStatus.NEW, get(triplet).status);
   }
 
   private String createDraftChange() throws GitAPIException, IOException {
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 3da0e6a..10fbed0 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
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static org.junit.Assert.assertEquals;
 
 import com.google.common.collect.Iterables;
@@ -24,90 +22,53 @@
 import com.google.gerrit.acceptance.PushOneCommit.Result;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeStatus;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
 
 public class DeleteDraftPatchSetIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private TestAccount user;
-
-  private RestSession userSession;
-  private Git git;
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    user = accounts.create("user", "user@example.com", "User");
-    userSession = new RestSession(server, user);
-    Project.NameKey project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    sshSession.close();
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
-  public void deletePatchSet() throws GitAPIException,
-      IOException, OrmException {
-    String changeId = createChangeWith2PS("refs/for/master");
+  public void deletePatchSet() throws Exception {
+    String changeId = createChange().getChangeId();
     PatchSet ps = getCurrentPatchSet(changeId);
-    ChangeInfo c = getChange(changeId);
-    assertEquals("p~master~" + changeId, c.id);
-    assertEquals(Change.Status.NEW, c.status);
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertEquals(triplet, c.id);
+    assertEquals(ChangeStatus.NEW, c.status);
     RestResponse r = deletePatchSet(changeId, ps, adminSession);
     assertEquals("Patch set is not a draft.", r.getEntityContent());
     assertEquals(409, r.getStatusCode());
   }
 
   @Test
-  public void deleteDraftPatchSetNoACL() throws GitAPIException,
-      IOException, OrmException {
-    String changeId = createChangeWith2PS("refs/drafts/master");
+  public void deleteDraftPatchSetNoACL() throws Exception {
+    String changeId = createDraftChangeWith2PS();
     PatchSet ps = getCurrentPatchSet(changeId);
-    ChangeInfo c = getChange(changeId);
-    assertEquals("p~master~" + changeId, c.id);
-    assertEquals(Change.Status.DRAFT, c.status);
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertEquals(triplet, c.id);
+    assertEquals(ChangeStatus.DRAFT, c.status);
     RestResponse r = deletePatchSet(changeId, ps, userSession);
     assertEquals("Not found", r.getEntityContent());
     assertEquals(404, r.getStatusCode());
   }
 
   @Test
-  public void deleteDraftPatchSetAndChange() throws GitAPIException,
-      IOException, OrmException {
-    String changeId = createChangeWith2PS("refs/drafts/master");
+  public void deleteDraftPatchSetAndChange() throws Exception {
+    String changeId = createDraftChangeWith2PS();
     PatchSet ps = getCurrentPatchSet(changeId);
-    ChangeInfo c = getChange(changeId);
-    assertEquals("p~master~" + changeId, c.id);
-    assertEquals(Change.Status.DRAFT, c.status);
+    String triplet = "p~master~" + changeId;
+    ChangeInfo c = get(triplet);
+    assertEquals(triplet, c.id);
+    assertEquals(ChangeStatus.DRAFT, c.status);
     RestResponse r = deletePatchSet(changeId, ps, adminSession);
     assertEquals(204, r.getStatusCode());
     Change change = Iterables.getOnlyElement(db.changes().byKey(
@@ -121,13 +82,13 @@
         .toList().size());
   }
 
-  private String createChangeWith2PS(String ref) throws GitAPIException,
+  private String createDraftChangeWith2PS() throws GitAPIException,
       IOException {
     PushOneCommit push = pushFactory.create(db, admin.getIdent());
-    Result result = push.to(git, ref);
+    Result result = push.to(git, "refs/drafts/master");
     push = pushFactory.create(db, admin.getIdent(), PushOneCommit.SUBJECT,
         "b.txt", "4711", result.getChangeId());
-    return push.to(git, ref).getChangeId();
+    return push.to(git, "refs/drafts/master").getChangeId();
   }
 
   private PatchSet getCurrentPatchSet(String changeId) throws OrmException {
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 3cecfc5..97ae2fd 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
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.extensions.common.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.common.ListChangesOption.CURRENT_REVISION;
 import static org.junit.Assert.assertEquals;
@@ -24,43 +22,23 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
+import com.google.gerrit.extensions.common.ChangeInfo;
 
-import org.eclipse.jgit.api.Git;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 import java.util.List;
 
+@NoHttpd
 public class ListChangesOptionsIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  protected PushOneCommit.Factory pushFactory;
-
-  private Project.NameKey project;
-  private Git git;
-  private ReviewDb db;
   private String changeId;
   private List<PushOneCommit.Result> results;
 
   @Before
   public void setUp() throws Exception {
-    project = new Project.NameKey("p");
-    db = reviewDbProvider.open();
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-
     results = Lists.newArrayList();
     results.add(push("file contents", null));
     changeId = results.get(0).getChangeId();
@@ -79,21 +57,16 @@
     return r;
   }
 
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void noRevisionOptions() throws Exception {
-    ChangeInfo c = getChange(changeId);
+    ChangeInfo c = info(changeId);
     assertNull(c.currentRevision);
     assertNull(c.revisions);
   }
 
   @Test
   public void currentRevision() throws Exception {
-    ChangeInfo c = getChange(changeId, CURRENT_REVISION);
+    ChangeInfo c = get(changeId, CURRENT_REVISION);
     assertEquals(commitId(2), c.currentRevision);
     assertEquals(ImmutableSet.of(commitId(2)), c.revisions.keySet());
     assertEquals(3, c.revisions.get(commitId(2))._number);
@@ -101,7 +74,7 @@
 
   @Test
   public void allRevisions() throws Exception {
-    ChangeInfo c = getChange(changeId, ALL_REVISIONS);
+    ChangeInfo c = get(changeId, ALL_REVISIONS);
     assertEquals(commitId(2), c.currentRevision);
     assertEquals(ImmutableSet.of(commitId(0), commitId(1), commitId(2)),
         c.revisions.keySet());
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 7808ccb..f52104d 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
@@ -14,26 +14,15 @@
 
 package com.google.gerrit.acceptance.rest.change;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GerritConfig;
 import com.google.gerrit.acceptance.GerritConfigs;
-import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.SuggestReviewers.SuggestedReviewerInfo;
 import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -42,16 +31,6 @@
 
 public class SuggestReviewersIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Git git;
-  private ReviewDb db;
-  private Project.NameKey project;
-
   @Before
   public void setUp() throws Exception {
     group("users1");
@@ -61,25 +40,13 @@
     accounts.create("user1", "user1@example.com", "User1", "users1");
     accounts.create("user2", "user2@example.com", "User2", "users2");
     accounts.create("user3", "user3@example.com", "User3", "users1", "users2");
-
-    project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    sshSession.close();
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
   }
 
   @Test
   @GerritConfig(name = "suggest.accounts", value = "false")
   public void suggestReviewersNoResult1() throws GitAPIException, IOException,
       Exception {
-    String changeId = createChange(admin);
+    String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
     assertEquals(reviewers.size(), 0);
   }
@@ -92,7 +59,7 @@
       })
   public void suggestReviewersNoResult2() throws GitAPIException, IOException,
       Exception {
-    String changeId = createChange(admin);
+    String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
     assertEquals(reviewers.size(), 0);
   }
@@ -101,7 +68,7 @@
   @GerritConfig(name = "suggest.from", value = "2")
   public void suggestReviewersNoResult3() throws GitAPIException, IOException,
       Exception {
-    String changeId = createChange(admin);
+    String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
     assertEquals(reviewers.size(), 0);
   }
@@ -109,7 +76,7 @@
   @Test
   public void suggestReviewersChange() throws GitAPIException,
       IOException, Exception {
-    String changeId = createChange(admin);
+    String changeId = createChange().getChangeId();
     List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
     assertEquals(reviewers.size(), 6);
     reviewers = suggestReviewers(changeId, "u", 5);
@@ -136,10 +103,4 @@
   private void group(String name) throws IOException {
     adminSession.put("/groups/" + name, new Object()).consume();
   }
-
-  private String createChange(TestAccount account) throws GitAPIException,
-      IOException {
-    PushOneCommit push = pushFactory.create(db, account.getIdent());
-    return push.to(git, "refs/for/master").getChangeId();
-  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
index 50857ec..3646e57 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
@@ -89,12 +89,11 @@
 
   @Test
   public void addRemoveMember() throws Exception {
-    TestAccount u = accounts.create("user", "user@example.com", "Full Name");
     RestResponse r = PUT("/groups/Administrators/members/user");
     assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
     AccountInfo ai = newGson().fromJson(r.getReader(), AccountInfo.class);
-    assertAccountInfo(u, ai);
-    assertMembers("Administrators", admin, u);
+    assertAccountInfo(user, ai);
+    assertMembers("Administrators", admin, user);
     r.consume();
 
     assertEquals(HttpStatus.SC_NO_CONTENT,
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
index 90ebb06..a6954b2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.group.CreateGroup;
@@ -71,7 +70,6 @@
   @Test
   public void testCreateGroupWithoutCapability_Forbidden() throws OrmException,
       JSchException, IOException {
-    TestAccount user = accounts.create("user", "user@example.com", "User");
     RestResponse r = (new RestSession(server, user)).put("/groups/newGroup");
     assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
index f1d4f3c..0d229ca 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/DefaultGroupsIT.java
@@ -22,12 +22,9 @@
 import com.google.gerrit.acceptance.RestSession;
 import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.group.GroupJson.GroupInfo;
 import com.google.gson.reflect.TypeToken;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
 
 import com.jcraft.jsch.JSchException;
 
@@ -46,9 +43,6 @@
  */
 public class DefaultGroupsIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
   @Test
   public void defaultGroupsCreated_ssh() throws JSchException, IOException {
     SshSession session = new SshSession(server, admin);
@@ -72,16 +66,11 @@
 
   @Test
   public void defaultGroupsCreated_internals() throws OrmException {
-    ReviewDb db = reviewDbProvider.open();
-    try {
-      Set<String> names = Sets.newHashSet();
-      for (AccountGroup g : db.accountGroups().all()) {
-        names.add(g.getName());
-      }
-      assertTrue(names.contains("Administrators"));
-      assertTrue(names.contains("Non-Interactive Users"));
-    } finally {
-      db.close();
+    Set<String> names = Sets.newHashSet();
+    for (AccountGroup g : db.accountGroups().all()) {
+      names.add(g.getName());
     }
+    assertTrue(names.contains("Administrators"));
+    assertTrue(names.contains("Non-Interactive Users"));
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
index be601a5..8b5dde6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
@@ -24,8 +24,6 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
@@ -69,9 +67,6 @@
   @Test
   public void testOnlyVisibleGroupsReturned() throws OrmException,
       JSchException, IOException {
-    TestAccount user = accounts.create("user", "user@example.com", "User");
-    RestSession userSession = new RestSession(server, user);
-
     String newGroupName = "newGroup";
     CreateGroup.Input in = new CreateGroup.Input();
     in.description = "a hidden group";
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 116ea22..a4bcdf2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -14,19 +14,14 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -51,25 +46,11 @@
   @Inject
   private AllProjectsNameProvider allProjects;
 
-  private RestSession userSession;
-
-  private Project.NameKey project;
   private Branch.NameKey branch;
 
   @Before
   public void setUp() throws Exception {
-    TestAccount user = accounts.create("user", "user@example.com", "User");
-    userSession = new RestSession(server, user);
-
-    project = new Project.NameKey("p");
     branch = new Branch.NameKey(project, "test");
-
-    SshSession sshSession = new SshSession(server, admin);
-    try {
-      createProject(sshSession, project.get(), null, true);
-    } finally {
-      sshSession.close();
-    }
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
index 7177155..b593840 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -24,8 +24,6 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
@@ -194,8 +192,7 @@
   @Test
   public void testCreateProjectWithoutCapability_Forbidden() throws OrmException,
       JSchException, IOException {
-    TestAccount user = accounts.create("user", "user@example.com", "User");
-    RestResponse r = new RestSession(server, user).put("/projects/newProject");
+    RestResponse r = userSession.put("/projects/newProject");
     assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
index 5193be7..020fd43 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -14,19 +14,14 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -52,26 +47,11 @@
   @Inject
   private AllProjectsNameProvider allProjects;
 
-  private RestSession userSession;
-
-  private Project.NameKey project;
   private Branch.NameKey branch;
 
   @Before
   public void setUp() throws Exception {
-    TestAccount user = accounts.create("user", "user@example.com", "User");
-    userSession = new RestSession(server, user);
-
-    project = new Project.NameKey("p");
     branch = new Branch.NameKey(project, "test");
-
-    SshSession sshSession = new SshSession(server, admin);
-    try {
-      createProject(sshSession, project.get(), null, true);
-    } finally {
-      sshSession.close();
-    }
-
     adminSession.put("/projects/" + project.get()
         + "/branches/" + branch.getShortName()).consume();
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
index 9368593..26585ba 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -20,8 +20,6 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GcAssert;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.UseLocalDisk;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -49,14 +47,11 @@
 
   @Before
   public void setUp() throws Exception {
-    SshSession sshSession = new SshSession(server, admin);
-
     project1 = new Project.NameKey("p1");
     createProject(sshSession, project1.get());
 
     project2 = new Project.NameKey("p2");
     createProject(sshSession, project2.get());
-    sshSession.close();
   }
 
   @Test
@@ -65,10 +60,11 @@
   }
 
   @Test
-  public void testGcNotAllowed_Forbidden() throws IOException, OrmException, JSchException {
+  public void testGcNotAllowed_Forbidden() throws IOException, OrmException,
+      JSchException {
     assertEquals(HttpStatus.SC_FORBIDDEN,
-        new RestSession(server, accounts.create("user", "user@example.com", "User"))
-            .post("/projects/" + allProjects.get() + "/gc").getStatusCode());
+        userSession.post("/projects/" + allProjects.get() + "/gc")
+            .getStatusCode());
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
index 8b5b50a..52bb623 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListBranchesIT.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
 import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.rest.project.BranchAssert.assertBranches;
 import static org.junit.Assert.assertEquals;
@@ -23,13 +22,10 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
@@ -37,18 +33,14 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gson.reflect.TypeToken;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
 import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -63,32 +55,6 @@
   @Inject
   private ProjectCache projectCache;
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private SshSession sshSession;
-  private Project.NameKey project;
-  private Git git;
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    project = new Project.NameKey("p");
-    sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void cleanup() {
-    sshSession.close();
-    db.close();
-  }
-
   @Test
   public void listBranchesOfNonExistingProject_NotFound() throws IOException {
     assertEquals(HttpStatus.SC_NOT_FOUND,
@@ -99,9 +65,8 @@
   public void listBranchesOfNonVisibleProject_NotFound() throws IOException,
       OrmException, JSchException, ConfigInvalidException {
     blockRead(project, "refs/*");
-    RestSession session = new RestSession(server, accounts.user());
     assertEquals(HttpStatus.SC_NOT_FOUND,
-        session.get("/projects/" + project.get() + "/branches").getStatusCode());
+        userSession.get("/projects/" + project.get() + "/branches").getStatusCode());
   }
 
   @Test
@@ -145,12 +110,10 @@
   public void listBranchesSomeHidden() throws IOException, GitAPIException,
       ConfigInvalidException, OrmException, JSchException {
     blockRead(project, "refs/heads/dev");
-    RestSession session =
-        new RestSession(server, accounts.create("user", "user@example.com", "User"));
     pushTo("refs/heads/master");
     String masterCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
     pushTo("refs/heads/dev");
-    RestResponse r = session.get("/projects/" + project.get() + "/branches");
+    RestResponse r = userSession.get("/projects/" + project.get() + "/branches");
     // refs/meta/config is hidden since user is no project owner
     List<BranchInfo> expected = Lists.asList(
         new BranchInfo("HEAD", "master", false),
@@ -164,12 +127,10 @@
   public void listBranchesHeadHidden() throws IOException, GitAPIException,
       ConfigInvalidException, OrmException, JSchException {
     blockRead(project, "refs/heads/master");
-    RestSession session =
-        new RestSession(server, accounts.create("user", "user@example.com", "User"));
     pushTo("refs/heads/master");
     pushTo("refs/heads/dev");
     String devCommit = git.getRepository().getRef("master").getTarget().getObjectId().getName();
-    RestResponse r = session.get("/projects/" + project.get() + "/branches");
+    RestResponse r = userSession.get("/projects/" + project.get() + "/branches");
     // refs/meta/config is hidden since user is no project owner
     assertBranches(Collections.singletonList(new BranchInfo("refs/heads/dev",
         devCommit, false)), toBranchInfoList(r));
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index a275314..c61764b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -21,7 +21,6 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
@@ -52,27 +51,27 @@
   public void listNoChildren() throws IOException {
     RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertTrue(toProjectInfoList(r).isEmpty());
+    List<ProjectInfo> projectInfoList = toProjectInfoList(r);
+    // Project 'p' was already created in the base class
+    assertTrue(projectInfoList.size() == 1);
   }
 
   @Test
   public void listChildren() throws IOException, JSchException {
-    SshSession sshSession = new SshSession(server, admin);
+    Project.NameKey existingProject = new Project.NameKey("p");
     Project.NameKey child1 = new Project.NameKey("p1");
     createProject(sshSession, child1.get());
     Project.NameKey child2 = new Project.NameKey("p2");
     createProject(sshSession, child2.get());
     createProject(sshSession, "p1.1", child1);
-    sshSession.close();
 
     RestResponse r = GET("/projects/" + allProjects.get() + "/children/");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertProjects(Arrays.asList(child1, child2), toProjectInfoList(r));
+    assertProjects(Arrays.asList(existingProject, child1, child2), toProjectInfoList(r));
   }
 
   @Test
   public void listChildrenRecursively() throws IOException, JSchException {
-    SshSession sshSession = new SshSession(server, admin);
     Project.NameKey child1 = new Project.NameKey("p1");
     createProject(sshSession, child1.get());
     createProject(sshSession, "p2");
@@ -84,12 +83,11 @@
     createProject(sshSession, child1_1_1.get(), child1_1);
     Project.NameKey child1_1_1_1 = new Project.NameKey("p1.1.1.1");
     createProject(sshSession, child1_1_1_1.get(), child1_1_1);
-    sshSession.close();
 
     RestResponse r = GET("/projects/" + child1.get() + "/children/?recursive");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    assertProjects(Arrays.asList(child1_1, child1_2, child1_1_1, child1_1_1_1),
-        toProjectInfoList(r));
+    assertProjects(Arrays.asList(child1_1, child1_2,
+        child1_1_1, child1_1_1_1), toProjectInfoList(r));
   }
 
   private static List<ProjectInfo> toProjectInfoList(RestResponse r)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
index 2510a03..7c6f280 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
@@ -19,9 +19,6 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.RestSession;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.project.SetParent;
@@ -30,8 +27,6 @@
 import com.jcraft.jsch.JSchException;
 
 import org.apache.http.HttpStatus;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -41,32 +36,12 @@
   @Inject
   private AllProjectsNameProvider allProjects;
 
-  private RestSession userSession;
-  private SshSession sshSession;
-
-  private String project;
-
-  @Before
-  public void setUp() throws Exception {
-    TestAccount user = accounts.user();
-    userSession = new RestSession(server, user);
-
-    sshSession = new SshSession(server, admin);
-    project = "p";
-    createProject(sshSession, project, null, true);
-  }
-
-  @After
-  public void cleanup() {
-    sshSession.close();
-  }
-
   @Test
   public void setParent_Forbidden() throws IOException, JSchException {
     String parent = "parent";
     createProject(sshSession, parent, null, true);
     RestResponse r =
-        userSession.put("/projects/" + project + "/parent",
+        userSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
     assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
     r.consume();
@@ -77,12 +52,12 @@
     String parent = "parent";
     createProject(sshSession, parent, null, true);
     RestResponse r =
-        adminSession.put("/projects/" + project + "/parent",
+        adminSession.put("/projects/" + project.get() + "/parent",
             newParentInput(parent));
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     r.consume();
 
-    r = adminSession.get("/projects/" + project + "/parent");
+    r = adminSession.get("/projects/" + project.get() + "/parent");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     String newParent =
         newGson().fromJson(r.getReader(), String.class);
@@ -94,7 +69,7 @@
   public void setParentForAllProjects_Conflict() throws IOException {
     RestResponse r =
         adminSession.put("/projects/" + allProjects.get() + "/parent",
-            newParentInput(project));
+            newParentInput(project.get()));
     assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
     r.consume();
   }
@@ -102,21 +77,21 @@
   @Test
   public void setInvalidParent_Conflict() throws IOException, JSchException {
     RestResponse r =
-        adminSession.put("/projects/" + project + "/parent",
-            newParentInput(project));
+        adminSession.put("/projects/" + project.get() + "/parent",
+            newParentInput(project.get()));
     assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
     r.consume();
 
     String child = "child";
-    createProject(sshSession, child, new Project.NameKey(project), true);
-    r = adminSession.put("/projects/" + project + "/parent",
+    createProject(sshSession, child, project, true);
+    r = adminSession.put("/projects/" + project.get() + "/parent",
            newParentInput(child));
     assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
     r.consume();
 
     String grandchild = "grandchild";
     createProject(sshSession, grandchild, new Project.NameKey(child), true);
-    r = adminSession.put("/projects/" + project + "/parent",
+    r = adminSession.put("/projects/" + project.get() + "/parent",
            newParentInput(grandchild));
     assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
     r.consume();
@@ -125,7 +100,7 @@
   @Test
   public void setNonExistingParent_UnprocessibleEntity() throws IOException {
     RestResponse r =
-        adminSession.put("/projects/" + project + "/parent",
+        adminSession.put("/projects/" + project.get() + "/parent",
             newParentInput("non-existing"));
     assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
     r.consume();
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 dbd34fe..1456086 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,9 +15,7 @@
 package com.google.gerrit.acceptance.server.change;
 
 import static com.google.gerrit.acceptance.GitUtil.add;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
 import static com.google.gerrit.acceptance.GitUtil.createCommit;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static org.junit.Assert.assertEquals;
 
@@ -26,22 +24,14 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil.Commit;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.GetRelated.ChangeAndCommit;
 import com.google.gerrit.server.change.GetRelated.RelatedInfo;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.ResetCommand.ResetType;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.junit.After;
-import org.junit.Before;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -49,30 +39,6 @@
 
 public class GetRelatedIT extends AbstractDaemonTest {
 
-  @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Git git;
-  private ReviewDb db;
-
-  @Before
-  public void setUp() throws Exception {
-    Project.NameKey project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    createProject(sshSession, project.get());
-    git = cloneProject(sshSession.getUrl() + "/" + project.get());
-    sshSession.close();
-    db = reviewDbProvider.open();
-  }
-
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void getRelatedNoResult() throws GitAPIException,
       IOException, Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
index 44f3771..ce722fa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
@@ -15,65 +15,35 @@
 package com.google.gerrit.acceptance.server.project;
 
 import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.gerrit.acceptance.GitUtil.cloneProject;
-import static com.google.gerrit.acceptance.GitUtil.createProject;
 import static com.google.gerrit.extensions.common.ListChangesOption.DETAILED_LABELS;
-import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
-import static com.google.gerrit.server.project.Util.grant;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.SshSession;
-import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.RevisionApi;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
-import com.google.gerrit.server.change.ChangeJson.LabelInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
-import com.google.inject.util.Providers;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.Repository;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
+@NoHttpd
 public class LabelTypeIT extends AbstractDaemonTest {
 
   @Inject
-  private SchemaFactory<ReviewDb> reviewDbProvider;
-
-  @Inject
   private GitRepositoryManager repoManager;
 
   @Inject
-  private GerritApi gApi;
-
-  @Inject
-  private AcceptanceTestRequestScope atrScope;
-
-  @Inject
-  private IdentifiedUser.GenericFactory identifiedUserFactory;
-
-  @Inject
   private ProjectCache projectCache;
 
   @Inject
@@ -82,36 +52,11 @@
   @Inject
   private MetaDataUpdate.Server metaDataUpdateFactory;
 
-  @Inject
-  private PushOneCommit.Factory pushFactory;
-
-  private Project.NameKey project;
   private LabelType codeReview;
-  private TestAccount user;
-  private Git git;
-  private ReviewDb db;
 
   @Before
   public void setUp() throws Exception {
-    user = accounts.user();
-    project = new Project.NameKey("p");
-    SshSession sshSession = new SshSession(server, admin);
-    try {
-      createProject(sshSession, project.get());
-      git = cloneProject(sshSession.getUrl() + "/" + project.get());
-      db = reviewDbProvider.open();
-      atrScope.set(atrScope.newContext(reviewDbProvider, sshSession,
-          identifiedUserFactory.create(Providers.of(db), user.getId())));
-    } finally {
-      sshSession.close();
-    }
-
     ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    AccountGroup.UUID anonymousUsers =
-        SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
-    grant(cfg, Permission.forLabel("Code-Review"), -2, 2, anonymousUsers,
-        "refs/heads/*");
-    grant(cfg, Permission.SUBMIT, anonymousUsers, "refs/heads/*");
     codeReview = checkNotNull(cfg.getLabelSections().get("Code-Review"));
     codeReview.setCopyMinScore(false);
     codeReview.setCopyMaxScore(false);
@@ -120,120 +65,69 @@
     saveProjectConfig(cfg);
   }
 
-  @After
-  public void cleanup() {
-    db.close();
-  }
-
   @Test
   public void noCopyMinScoreOnRework() throws Exception {
-    String subject = "test commit";
-    String file = "a.txt";
-
-    PushOneCommit push = pushFactory.create(db, user.getIdent(),
-        subject, file, "first contents");
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit.Result r = createChange();
     revision(r).review(ReviewInput.reject());
     assertApproval(r, -2);
-
-    push = pushFactory.create(db, user.getIdent(),
-        subject, file, "second contents", r.getChangeId());
-    r = push.to(git, "refs/for/master");
+    r = ammendChange(r.getChangeId());
     assertApproval(r, 0);
   }
 
   @Test
   public void copyMinScoreOnRework() throws Exception {
-    String subject = "test commit";
-    String file = "a.txt";
     codeReview.setCopyMinScore(true);
     saveLabelConfig();
-
-    PushOneCommit push = pushFactory.create(db, user.getIdent(),
-        subject, file, "first contents");
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit.Result r = createChange();
     revision(r).review(ReviewInput.reject());
     assertApproval(r, -2);
-
-    push = pushFactory.create(db, user.getIdent(),
-        subject, file, "second contents", r.getChangeId());
-    r = push.to(git, "refs/for/master");
+    r = ammendChange(r.getChangeId());
     assertApproval(r, -2);
   }
 
   @Test
   public void noCopyMaxScoreOnRework() throws Exception {
-    String subject = "test commit";
-    String file = "a.txt";
-
-    PushOneCommit push = pushFactory.create(db, user.getIdent(),
-        subject, file, "first contents");
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit.Result r = createChange();
     revision(r).review(ReviewInput.approve());
     assertApproval(r, 2);
-
-    push = pushFactory.create(db, user.getIdent(),
-        subject, file, "second contents", r.getChangeId());
-    r = push.to(git, "refs/for/master");
+    r = ammendChange(r.getChangeId());
     assertApproval(r, 0);
   }
 
   @Test
   public void copyMaxScoreOnRework() throws Exception {
-    String subject = "test commit";
-    String file = "a.txt";
     codeReview.setCopyMaxScore(true);
     saveLabelConfig();
-
-    PushOneCommit push = pushFactory.create(db, user.getIdent(),
-        subject, file, "first contents");
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit.Result r = createChange();
     revision(r).review(ReviewInput.approve());
     assertApproval(r, 2);
-
-    push = pushFactory.create(db, user.getIdent(),
-        subject, file, "second contents", r.getChangeId());
-    r = push.to(git, "refs/for/master");
+    r = ammendChange(r.getChangeId());
     assertApproval(r, 2);
   }
 
   @Test
   public void noCopyNonMaxScoreOnRework() throws Exception {
-    String subject = "test commit";
-    String file = "a.txt";
     codeReview.setCopyMinScore(true);
     codeReview.setCopyMaxScore(true);
     saveLabelConfig();
 
-    PushOneCommit push = pushFactory.create(db, user.getIdent(),
-        subject, file, "first contents");
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit.Result r = createChange();
     revision(r).review(ReviewInput.recommend());
     assertApproval(r, 1);
-
-    push = pushFactory.create(db, user.getIdent(),
-        subject, file, "second contents", r.getChangeId());
-    r = push.to(git, "refs/for/master");
+    r = ammendChange(r.getChangeId());
     assertApproval(r, 0);
   }
 
   @Test
   public void noCopyNonMinScoreOnRework() throws Exception {
-    String subject = "test commit";
-    String file = "a.txt";
     codeReview.setCopyMinScore(true);
     codeReview.setCopyMaxScore(true);
     saveLabelConfig();
 
-    PushOneCommit push = pushFactory.create(db, user.getIdent(),
-        subject, file, "first contents");
-    PushOneCommit.Result r = push.to(git, "refs/for/master");
+    PushOneCommit.Result r = createChange();
     revision(r).review(ReviewInput.dislike());
     assertApproval(r, -1);
-
-    push = pushFactory.create(db, user.getIdent(),
-        subject, file, "second contents", r.getChangeId());
-    r = push.to(git, "refs/for/master");
+    r = ammendChange(r.getChangeId());
     assertApproval(r, 0);
   }
 
@@ -242,13 +136,13 @@
     String file = "a.txt";
     String contents = "contents";
 
-    PushOneCommit push = pushFactory.create(db, user.getIdent(),
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
         "first subject", file, contents);
     PushOneCommit.Result r = push.to(git, "refs/for/master");
     revision(r).review(ReviewInput.recommend());
     assertApproval(r, 1);
 
-    push = pushFactory.create(db, user.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(),
         "second subject", file, contents, r.getChangeId());
     r = push.to(git, "refs/for/master");
     assertApproval(r, 0);
@@ -261,13 +155,13 @@
     codeReview.setCopyAllScoresIfNoCodeChange(true);
     saveLabelConfig();
 
-    PushOneCommit push = pushFactory.create(db, user.getIdent(),
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(),
         "first subject", file, contents);
     PushOneCommit.Result r = push.to(git, "refs/for/master");
     revision(r).review(ReviewInput.recommend());
     assertApproval(r, 1);
 
-    push = pushFactory.create(db, user.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(),
         "second subject", file, contents, r.getChangeId());
     r = push.to(git, "refs/for/master");
     assertApproval(r, 1);
@@ -279,17 +173,17 @@
     String file = "a.txt";
     String contents = "contents";
 
-    PushOneCommit push = pushFactory.create(db, user.getIdent());
+    PushOneCommit push = pushFactory.create(db, admin.getIdent());
     PushOneCommit.Result r1 = push.to(git, "refs/for/master");
     merge(r1);
 
-    push = pushFactory.create(db, user.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(),
         "non-conflicting", "b.txt", "other contents");
     PushOneCommit.Result r2 = push.to(git, "refs/for/master");
     merge(r2);
 
     git.checkout().setName(r1.getCommit().name()).call();
-    push = pushFactory.create(db, user.getIdent(), subject, file, contents);
+    push = pushFactory.create(db, admin.getIdent(), subject, file, contents);
     PushOneCommit.Result r3 = push.to(git, "refs/for/master");
     revision(r3).review(ReviewInput.recommend());
     assertApproval(r3, 1);
@@ -306,17 +200,17 @@
     codeReview.setCopyAllScoresOnTrivialRebase(true);
     saveLabelConfig();
 
-    PushOneCommit push = pushFactory.create(db, user.getIdent());
+    PushOneCommit push = pushFactory.create(db, admin.getIdent());
     PushOneCommit.Result r1 = push.to(git, "refs/for/master");
     merge(r1);
 
-    push = pushFactory.create(db, user.getIdent(),
+    push = pushFactory.create(db, admin.getIdent(),
         "non-conflicting", "b.txt", "other contents");
     PushOneCommit.Result r2 = push.to(git, "refs/for/master");
     merge(r2);
 
     git.checkout().setName(r1.getCommit().name()).call();
-    push = pushFactory.create(db, user.getIdent(), subject, file, contents);
+    push = pushFactory.create(db, admin.getIdent(), subject, file, contents);
     PushOneCommit.Result r3 = push.to(git, "refs/for/master");
     revision(r3).review(ReviewInput.recommend());
     assertApproval(r3, 1);
@@ -344,7 +238,7 @@
   private RevisionApi revision(PushOneCommit.Result r) throws Exception {
     return gApi.changes()
         .id(r.getChangeId())
-        .revision(r.getCommit().name());
+        .current();
   }
 
   private void merge(PushOneCommit.Result r) throws Exception {
@@ -367,13 +261,10 @@
       throws Exception {
     // Don't use asserts from PushOneCommit so we can test the round-trip
     // through JSON instead of querying the DB directly.
-    LabelInfo cr = getChange(r).labels.get("Code-Review");
+    ChangeInfo c = get(r.getChangeId(), DETAILED_LABELS);
+    LabelInfo cr = c.labels.get("Code-Review");
     assertEquals(1, cr.all.size());
-    assertEquals("User", cr.all.get(0).name);
+    assertEquals("Administrator", cr.all.get(0).name);
     assertEquals(expected, cr.all.get(0).value.intValue());
   }
-
-  private ChangeInfo getChange(PushOneCommit.Result pr) throws IOException {
-    return getChange(pr.getChangeId(), DETAILED_LABELS);
-  }
 }
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 ddc78ab..9f859dc 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
@@ -33,7 +33,6 @@
 
 import com.jcraft.jsch.JSchException;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -55,15 +54,12 @@
   @Inject
   private GcAssert gcAssert;
 
-  private SshSession sshSession;
   private Project.NameKey project1;
   private Project.NameKey project2;
   private Project.NameKey project3;
 
   @Before
   public void setUp() throws Exception {
-    sshSession = new SshSession(server, admin);
-
     project1 = new Project.NameKey("p1");
     createProject(sshSession, project1.get());
 
@@ -74,11 +70,6 @@
     createProject(sshSession, project3.get());
   }
 
-  @After
-  public void cleanup() {
-    sshSession.close();
-  }
-
   @Test
   @UseLocalDisk
   public void testGc() throws JSchException, IOException {
@@ -103,7 +94,7 @@
   @Test
   public void testGcWithoutCapability_Error() throws IOException, OrmException,
       JSchException {
-    SshSession s = new SshSession(server, accounts.create("user", "user@example.com", "User"));
+    SshSession s = new SshSession(server, user);
     s.exec("gerrit gc --all");
     assertError("Capability runGC is required to access this resource", s.getError());
     s.close();
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index deb6dc3..e4199c2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -35,9 +35,8 @@
   public static final String SETTINGS_NEW_AGREEMENT = "/settings/new-agreement";
   public static final String REGISTER = "/register";
 
-  public static final String TOP = "n,z";
-
   public static final String MINE = "/";
+  public static final String QUERY = "/q/";
   public static final String PROJECTS = "/projects/";
   public static final String DASHBOARDS = ",dashboards/";
   public static final String ADMIN_GROUPS = "/admin/groups/";
@@ -84,7 +83,7 @@
   }
 
   public static String toAccountQuery(String fullname, Status status) {
-    return toChangeQuery(op("owner", fullname) + " " + status(status), TOP);
+    return toChangeQuery(op("owner", fullname) + " " + status(status));
   }
 
   public static String toCustomDashboard(final String params) {
@@ -95,12 +94,13 @@
     return ADMIN_PROJECTS + proj.get() + ",dashboards";
   }
 
-  public static String toChangeQuery(final String query) {
-    return toChangeQuery(query, TOP);
+  public static String toChangeQuery(String query) {
+    return QUERY + KeyUtil.encode(query);
   }
 
-  public static String toChangeQuery(String query, String page) {
-    return "/q/" + KeyUtil.encode(query) + "," + page;
+  public static String toChangeQuery(String query, String start) {
+    int s = Integer.parseInt(start);
+    return QUERY + KeyUtil.encode(query) + (s > 0 ? "," + s : "");
   }
 
   public static String toProjectDashboard(Project.NameKey name, String id) {
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 3d06d7c..f0a9e4d 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
@@ -40,4 +40,9 @@
   void addReviewer(String in) throws RestApiException;
 
   ChangeInfo get(EnumSet<ListChangesOption> options) throws RestApiException;
+
+  /** {@code get} with {@link ListChangesOption} set to ALL. */
+  ChangeInfo get() throws RestApiException;
+  /** {@code get} with {@link ListChangesOption} set to NONE. */
+  ChangeInfo info() throws RestApiException;
 }
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
index 7ba2a3e..09a7fb6 100644
--- a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
+++ b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
@@ -202,7 +202,7 @@
 
     @Override
     public String getName() {
-      return this.getName();
+      return this.getClass().getName();
     }
 
     /**
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 3879df9..a1ca5a1 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
@@ -22,6 +22,7 @@
 import static com.google.gerrit.common.PageLinks.DASHBOARDS;
 import static com.google.gerrit.common.PageLinks.MINE;
 import static com.google.gerrit.common.PageLinks.PROJECTS;
+import static com.google.gerrit.common.PageLinks.QUERY;
 import static com.google.gerrit.common.PageLinks.REGISTER;
 import static com.google.gerrit.common.PageLinks.SETTINGS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_AGREEMENTS;
@@ -34,6 +35,7 @@
 import static com.google.gerrit.common.PageLinks.SETTINGS_SSHKEYS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_WEBIDENT;
 import static com.google.gerrit.common.PageLinks.op;
+import static com.google.gerrit.common.PageLinks.toChangeQuery;
 
 import com.google.gerrit.client.account.MyAgreementsScreen;
 import com.google.gerrit.client.account.MyContactInformationScreen;
@@ -83,8 +85,8 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -218,7 +220,7 @@
   }
 
   private static void select(final String token) {
-    if (matchPrefix("/q/", token)) {
+    if (matchPrefix(QUERY, token)) {
       query(token);
 
     } else if (matchPrefix("/Documentation/", token)) {
@@ -291,19 +293,19 @@
     }
 
     if (matchExact("mine,starred", token)) {
-      return PageLinks.toChangeQuery("is:starred");
+      return toChangeQuery("is:starred");
     }
 
     if (matchExact("mine,drafts", token)) {
-      return PageLinks.toChangeQuery("is:draft");
+      return toChangeQuery("is:draft");
     }
 
     if (matchExact("mine,comments", token)) {
-      return PageLinks.toChangeQuery("has:draft");
+      return toChangeQuery("has:draft");
     }
 
     if (matchPrefix("mine,watched,", token)) {
-      return PageLinks.toChangeQuery("is:watched status:open", skip(token));
+      return toChangeQuery("is:watched status:open");
     }
 
     return null;
@@ -311,15 +313,15 @@
 
   private static String legacyAll(final String token) {
     if (matchPrefix("all,abandoned,", token)) {
-      return PageLinks.toChangeQuery("status:abandoned", skip(token));
+      return toChangeQuery("status:abandoned");
     }
 
     if (matchPrefix("all,merged,", token)) {
-      return PageLinks.toChangeQuery("status:merged", skip(token));
+      return toChangeQuery("status:merged");
     }
 
     if (matchPrefix("all,open,", token)) {
-      return PageLinks.toChangeQuery("status:open", skip(token));
+      return toChangeQuery("status:open");
     }
 
     return null;
@@ -330,27 +332,21 @@
       final String s = skip(token);
       final int c = s.indexOf(',');
       Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
-      return PageLinks.toChangeQuery( //
-          "status:open " + op("project", proj.get()), //
-          s.substring(c + 1));
+      return toChangeQuery("status:open " + op("project", proj.get()));
     }
 
     if (matchPrefix("project,merged,", token)) {
       final String s = skip(token);
       final int c = s.indexOf(',');
       Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
-      return PageLinks.toChangeQuery( //
-          "status:merged " + op("project", proj.get()), //
-          s.substring(c + 1));
+      return toChangeQuery("status:merged " + op("project", proj.get()));
     }
 
     if (matchPrefix("project,abandoned,", token)) {
       final String s = skip(token);
       final int c = s.indexOf(',');
       Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
-      return PageLinks.toChangeQuery( //
-          "status:abandoned " + op("project", proj.get()), //
-          s.substring(c + 1));
+      return toChangeQuery("status:abandoned " + op("project", proj.get()));
     }
 
     return null;
@@ -408,10 +404,22 @@
     return null;
   }
 
-  private static void query(final String token) {
-    final String s = skip(token);
-    final int c = s.indexOf(',');
-    Gerrit.display(token, new QueryScreen(s.substring(0, c), s.substring(c + 1)));
+  private static void query(String token) {
+    String s = skip(token);
+    int c = s.indexOf(',');
+    Screen screen;
+    if (c >= 0) {
+      String prefix = s.substring(0, c);
+      if (s.substring(c).equals(",n,z")) {
+        // Respect legacy token with max sortkey.
+        screen = new QueryScreen(prefix, 0);
+      } else {
+        screen = new QueryScreen(prefix, Integer.parseInt(s.substring(c + 1)));
+      }
+    } else {
+      screen = new QueryScreen(s, 0);
+    }
+    Gerrit.display(token, screen);
   }
 
   private static Screen mine(final String token) {
@@ -462,7 +470,7 @@
           @Override
           public void onFailure(Throwable caught) {
             if ("default".equals(dashboardId) && RestApi.isNotFound(caught)) {
-              Gerrit.display(PageLinks.toChangeQuery(
+              Gerrit.display(toChangeQuery(
                   PageLinks.projectQuery(new Project.NameKey(project))));
             } else {
               super.onFailure(caught);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index 8937bcc..3319457 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -61,6 +61,9 @@
   @Source("readOnly.png")
   public ImageResource readOnly();
 
+  @Source("gear.png")
+  public ImageResource gear();
+
   @Source("info.png")
   public ImageResource info();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 2f26ad9..1458976 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -110,8 +110,10 @@
   String plugins();
   String pluginEnabled();
   String pluginDisabled();
+  String pluginSettingsToolTip();
 
   String columnPluginName();
+  String columnPluginSettings();
   String columnPluginVersion();
   String columnPluginStatus();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index f06d657..14dbab2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -89,7 +89,9 @@
 plugins = Plugins
 pluginEnabled = Enabled
 pluginDisabled = Disabled
+pluginSettingsToolTip = Plugin Settings
 columnPluginName = Plugin Name
+columnPluginSettings = Settings
 columnPluginVersion = Version
 columnPluginStatus = Status
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
index 7f85f51..e1c73fa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
@@ -15,14 +15,17 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.api.ExtensionScreen;
 import com.google.gerrit.client.plugins.PluginInfo;
 import com.google.gerrit.client.plugins.PluginMap;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.ImageResourceRenderer;
 import com.google.gwt.user.client.ui.Panel;
 
 public class PluginListScreen extends PluginScreen {
@@ -60,13 +63,15 @@
   private class PluginTable extends FancyFlexTable<PluginInfo> {
     PluginTable() {
       table.setText(0, 1, Util.C.columnPluginName());
-      table.setText(0, 2, Util.C.columnPluginVersion());
-      table.setText(0, 3, Util.C.columnPluginStatus());
+      table.setText(0, 2, Util.C.columnPluginSettings());
+      table.setText(0, 3, Util.C.columnPluginVersion());
+      table.setText(0, 4, Util.C.columnPluginStatus());
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
       fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
       fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
       fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
     }
 
     void display(final PluginMap plugins) {
@@ -86,16 +91,20 @@
       if (plugin.disabled() || plugin.indexUrl() == null) {
         table.setText(row, 1, plugin.name());
       } else {
-        table.setWidget(
-            row,
-            1,
-            new Anchor(
-                plugin.name(),
-                Gerrit.selfRedirect(plugin.indexUrl()),
-                "_blank"));
+        table.setWidget(row, 1, new Anchor(plugin.name(),
+            Gerrit.selfRedirect(plugin.indexUrl()), "_blank"));
+
+        if (new ExtensionScreen(plugin.name() + "/settings").isFound()) {
+          InlineHyperlink adminScreenLink = new InlineHyperlink();
+          adminScreenLink.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.gear()));
+          adminScreenLink.setTargetHistoryToken("/x/" + plugin.name() + "/settings");
+          adminScreenLink.setTitle(Util.C.pluginSettingsToolTip());
+          table.setWidget(row, 2, adminScreenLink);
+        }
       }
-      table.setText(row, 2, plugin.version());
-      table.setText(row, 3, plugin.disabled()
+
+      table.setText(row, 3, plugin.version());
+      table.setText(row, 4, plugin.disabled()
           ? Util.C.pluginDisabled()
           : Util.C.pluginEnabled());
 
@@ -103,6 +112,7 @@
       fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
       fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
       fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
 
       setRowItem(row, plugin);
     }
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 2ca424c..57ba4b9 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
@@ -18,18 +18,20 @@
 import com.google.gerrit.client.ConfirmationDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountInfo;
-import com.google.gerrit.client.changes.ApprovalTable.PostInput;
-import com.google.gerrit.client.changes.ApprovalTable.PostResult;
 import com.google.gerrit.client.changes.ChangeApi;
 import com.google.gerrit.client.changes.ChangeInfo;
 import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -57,7 +59,7 @@
 import java.util.Set;
 
 /** Add reviewers. */
-class Reviewers extends Composite {
+public class Reviewers extends Composite {
   interface Binder extends UiBinder<HTMLPanel, Reviewers> {}
   private static final Binder uiBinder = GWT.create(Binder.class);
 
@@ -262,4 +264,43 @@
     }
     return d;
   }
+
+
+  public static class PostInput extends JavaScriptObject {
+    public static PostInput create(String reviewer, boolean confirmed) {
+      PostInput input = createObject().cast();
+      input.init(reviewer, confirmed);
+      return input;
+    }
+
+    private native void init(String reviewer, boolean confirmed) /*-{
+      this.reviewer = reviewer;
+      if (confirmed) {
+        this.confirmed = true;
+      }
+    }-*/;
+
+    protected PostInput() {
+    }
+  }
+
+  public static class ReviewerInfo extends AccountInfo {
+    final Set<String> approvals() {
+      return Natives.keys(_approvals());
+    }
+    final native String approval(String l) /*-{ return this.approvals[l]; }-*/;
+    private final native NativeMap<NativeString> _approvals() /*-{ return this.approvals; }-*/;
+
+    protected ReviewerInfo() {
+    }
+  }
+
+  public static class PostResult extends JavaScriptObject {
+    public final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/;
+    public final native boolean confirm() /*-{ return this.confirm || false; }-*/;
+    public final native String error() /*-{ return this.error; }-*/;
+
+    protected PostResult() {
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index 0ac487b..fbbff76 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -21,11 +21,11 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.change.Reviewers.PostInput;
+import com.google.gerrit.client.change.Reviewers.PostResult;
 import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountLinkPanel;
 import com.google.gerrit.client.ui.AddMemberBox;
@@ -35,7 +35,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArray;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -251,44 +250,6 @@
     }
   }
 
-  public static class PostInput extends JavaScriptObject {
-    public static PostInput create(String reviewer, boolean confirmed) {
-      PostInput input = createObject().cast();
-      input.init(reviewer, confirmed);
-      return input;
-    }
-
-    private native void init(String reviewer, boolean confirmed) /*-{
-      this.reviewer = reviewer;
-      if (confirmed) {
-        this.confirmed = true;
-      }
-    }-*/;
-
-    protected PostInput() {
-    }
-  }
-
-  public static class ReviewerInfo extends AccountInfo {
-    final Set<String> approvals() {
-      return Natives.keys(_approvals());
-    }
-    final native String approval(String l) /*-{ return this.approvals[l]; }-*/;
-    private final native NativeMap<NativeString> _approvals() /*-{ return this.approvals; }-*/;
-
-    protected ReviewerInfo() {
-    }
-  }
-
-  public static class PostResult extends JavaScriptObject {
-    public final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/;
-    public final native boolean confirm() /*-{ return this.confirm || false; }-*/;
-    public final native String error() /*-{ return this.error; }-*/;
-
-    protected PostResult() {
-    }
-  }
-
   private void addReviewer(final String reviewer, boolean confirmed) {
     ChangeApi.reviewers(lastChange.legacy_id().get()).post(
         PostInput.create(reviewer, confirmed),
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 ab9592c..97ba3b1 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
@@ -51,30 +51,16 @@
     call.get(callback);
   }
 
-  public static void prev(String query,
-      int limit, String sortkey,
-      AsyncCallback<ChangeList> callback) {
-    RestApi call = newQuery(query);
-    if (limit > 0) {
-      call.addParameter("n", limit);
-    }
-    addOptions(call, EnumSet.of(ListChangesOption.LABELS));
-    if (!PagedSingleListScreen.MIN_SORTKEY.equals(sortkey)) {
-      call.addParameter("P", sortkey);
-    }
-    call.get(callback);
-  }
-
   public static void next(String query,
-      int limit, String sortkey,
+      int start, int limit,
       AsyncCallback<ChangeList> callback) {
     RestApi call = newQuery(query);
     if (limit > 0) {
       call.addParameter("n", limit);
     }
     addOptions(call, EnumSet.of(ListChangesOption.LABELS));
-    if (!PagedSingleListScreen.MAX_SORTKEY.equals(sortkey)) {
-      call.addParameter("N", sortkey);
+    if (start != 0) {
+      call.addParameter("S", start);
     }
     call.get(callback);
   }
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 34b6dee..ac68722 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
@@ -95,7 +95,7 @@
 
     if (queries.size() == 1) {
       ChangeList.next(queries.get(0),
-          0, PagedSingleListScreen.MAX_SORTKEY,
+          0, 0,
           new GerritCallback<ChangeList>() {
             @Override
             public void onSuccess(ChangeList result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index 8cfb09c..f98ac78 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -26,25 +26,19 @@
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
 public abstract class PagedSingleListScreen extends Screen {
-  protected static final String MIN_SORTKEY = "";
-  protected static final String MAX_SORTKEY = "z";
-
   protected final int pageSize;
+  protected final int start;
+  private final String anchorPrefix;
+
+  protected ChangeList changes;
   private ChangeTable2 table;
   private ChangeTable2.Section section;
-  protected Hyperlink prev;
-  protected Hyperlink next;
-  protected ChangeList changes;
+  private Hyperlink prev;
+  private Hyperlink next;
 
-  protected final String anchorPrefix;
-  protected boolean useLoadPrev;
-  protected String pos;
-
-  protected PagedSingleListScreen(final String anchorToken,
-      final String positionToken) {
+  protected PagedSingleListScreen(String anchorToken, int start) {
     anchorPrefix = anchorToken;
-    useLoadPrev = positionToken.startsWith("p,");
-    pos = positionToken.substring(2);
+    this.start = start;
 
     if (Gerrit.isSignedIn()) {
       final AccountGeneralPreferences p =
@@ -96,25 +90,11 @@
   }
 
   @Override
-  protected void onLoad() {
-    super.onLoad();
-    if (useLoadPrev) {
-      loadPrev();
-    } else {
-      loadNext();
-    }
-  }
-
-  @Override
   public void registerKeys() {
     super.registerKeys();
     table.setRegisterKeys(true);
   }
 
-  protected abstract void loadPrev();
-
-  protected abstract void loadNext();
-
   protected AsyncCallback<ChangeList> loadCallback() {
     return new ScreenLoadCallback<ChangeList>(this) {
       @Override
@@ -124,22 +104,20 @@
     };
   }
 
-  protected void display(final ChangeList result) {
+  protected void display(ChangeList result) {
     changes = result;
     if (changes.length() != 0) {
-      final ChangeInfo f = changes.get(0);
-      final ChangeInfo l = changes.get(changes.length() - 1);
-
-      prev.setTargetHistoryToken(anchorPrefix + ",p," + f._sortkey());
-      next.setTargetHistoryToken(anchorPrefix + ",n," + l._sortkey());
-
-      if (useLoadPrev) {
-        prev.setVisible(f._more_changes());
-        next.setVisible(!MIN_SORTKEY.equals(pos));
+      if (start > 0) {
+        int p = start - pageSize;
+        prev.setTargetHistoryToken(anchorPrefix + (p > 0 ? "," + p : ""));
+        prev.setVisible(true);
       } else {
-        prev.setVisible(!MAX_SORTKEY.equals(pos));
-        next.setVisible(l._more_changes());
+        prev.setVisible(false);
       }
+
+      int n = start + changes.length();
+      next.setTargetHistoryToken(anchorPrefix + "," + n);
+      next.setVisible(changes.get(changes.length() - 1)._more_changes());
     }
     table.updateColumnsForLabels(result);
     section.display(result);
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 12bcf63..488b34b 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
@@ -25,17 +25,17 @@
 public class QueryScreen extends PagedSingleListScreen implements
     ChangeListScreen {
   public static QueryScreen forQuery(String query) {
-    return forQuery(query, PageLinks.TOP);
+    return forQuery(query, 0);
   }
 
-  public static QueryScreen forQuery(String query, String position) {
-    return new QueryScreen(KeyUtil.encode(query), position);
+  public static QueryScreen forQuery(String query, int start) {
+    return new QueryScreen(KeyUtil.encode(query), start);
   }
 
   private final String query;
 
-  public QueryScreen(final String encQuery, final String positionToken) {
-    super("/q/" + encQuery, positionToken);
+  public QueryScreen(String encQuery, int start) {
+    super(PageLinks.QUERY + encQuery, start);
     query = KeyUtil.decode(encQuery);
   }
 
@@ -72,13 +72,9 @@
   }
 
   @Override
-  protected void loadPrev() {
-    ChangeList.prev(query, pageSize, pos, loadCallback());
-  }
-
-  @Override
-  protected void loadNext() {
-    ChangeList.next(query, pageSize, pos, loadCallback());
+  protected void onLoad() {
+    super.onLoad();
+    ChangeList.next(query, start, pageSize, loadCallback());
   }
 
   private static boolean isSingleQuery(String query) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gear.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gear.png
new file mode 100644
index 0000000..2f84e47
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gear.png
Binary files differ
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
index 7163983..402ea1b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
@@ -58,9 +58,9 @@
       q = Predicate.and(q, builder.sortkey_before("z"), builder.limit(2), visibleToMe);
 
       ChangeQueryRewriter rewriter = queryRewriter.get();
-      Predicate<ChangeData> s = rewriter.rewrite(q);
+      Predicate<ChangeData> s = rewriter.rewrite(q, 0);
       if (!(s instanceof ChangeDataSource)) {
-        s = rewriter.rewrite(Predicate.and(builder.status_open(), q));
+        s = rewriter.rewrite(Predicate.and(builder.status_open(), q), 0);
       }
 
       if (s instanceof ChangeDataSource) {
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 6aa3895..c2b0cc8 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
@@ -261,15 +261,15 @@
       p.print("  my $h = shift;\n");
       p.print("  my $q;\n");
       p.print("  if (!$h || $h eq 'HEAD') {\n");
-      p.print("    $q = qq{#q,project:$ENV{'GERRIT_PROJECT_NAME'},n,z};\n");
+      p.print("    $q = qq{#q,project:$ENV{'GERRIT_PROJECT_NAME'}};\n");
       p.print("  } elsif ($h =~ /^refs\\/heads\\/([-\\w]+)$/) {\n");
       p.print("    $q = qq{#q,project:$ENV{'GERRIT_PROJECT_NAME'}");
-      p.print("+branch:$1,n,z};\n"); // wrapped
+      p.print("+branch:$1};\n"); // wrapped
       p.print("  } elsif ($h =~ /^refs\\/changes\\/\\d{2}\\/(\\d+)\\/\\d+$/) ");
       p.print("{\n"); // wrapped
       p.print("    $q = qq{#/c/$1};\n");
       p.print("  } else {\n");
-      p.print("    $q = qq{#/q/$h,n,z};\n");
+      p.print("    $q = qq{#/q/$h};\n");
       p.print("  }\n");
       p.print("  my $r = qq{$ENV{'GERRIT_CONTEXT_PATH'}$q};\n");
       p.print("  push @{$feature{'actions'}{'default'}},\n");
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 26c4206..ce450dc 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
@@ -270,7 +270,7 @@
                     || "PUT".equals(req.getMethod()))) {
               @SuppressWarnings("unchecked")
               AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) c;
-              viewData = new ViewData(null, ac.create(rsrc, id));
+              viewData = new ViewData(viewData.pluginName, ac.create(rsrc, id));
               status = SC_CREATED;
             } else {
               throw e;
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 73b1be3..4150c82 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
@@ -48,6 +48,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SortKeyPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
@@ -134,6 +135,18 @@
     LUCENE_VERSIONS = versions.build();
   }
 
+  public static void setReady(SitePaths sitePaths, int version, boolean ready)
+      throws IOException {
+    try {
+      FileBasedConfig cfg =
+          LuceneVersionManager.loadGerritIndexConfig(sitePaths);
+      LuceneVersionManager.setReady(cfg, version, ready);
+      cfg.save();
+    } catch (ConfigInvalidException e) {
+      throw new IOException(e);
+    }
+  }
+
   static interface Factory {
     LuceneChangeIndex create(Schema<ChangeData> schema, String base);
   }
@@ -288,8 +301,8 @@
   }
 
   @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
-      throws QueryParseException {
+  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
+      int limit) throws QueryParseException {
     Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
     List<SubIndex> indexes = Lists.newArrayListWithCapacity(2);
     if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
@@ -298,36 +311,50 @@
     if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
       indexes.add(closedIndex);
     }
-    return new QuerySource(indexes, queryBuilder.toQuery(p), limit,
-        ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
+    return new QuerySource(indexes, queryBuilder.toQuery(p), start, limit,
+        getSort(schema, p));
   }
 
   @Override
   public void markReady(boolean ready) throws IOException {
-    try {
-      FileBasedConfig cfg = LuceneVersionManager.loadGerritIndexConfig(sitePaths);
-      LuceneVersionManager.setReady(cfg, schema.getVersion(), ready);
-      cfg.save();
-    } catch (ConfigInvalidException e) {
-      throw new IOException(e);
-    }
+    setReady(sitePaths, schema.getVersion(), ready);
   }
 
   private static final ImmutableSet<String> FIELDS =
       ImmutableSet.of(ID_FIELD, CHANGE_FIELD, APPROVAL_FIELD);
 
+  @SuppressWarnings("deprecation")
+  private static Sort getSort(Schema<ChangeData> schema,
+      Predicate<ChangeData> p) {
+    // Standard order is descending by sort key, unless reversed due to a
+    // sortkey_before predicate.
+    if (SortKeyPredicate.hasSortKeyField(schema)) {
+      boolean reverse = ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p);
+      return new Sort(new SortField(
+          ChangeField.SORTKEY.getName(), SortField.Type.LONG, !reverse));
+    } else {
+      return new Sort(
+          new SortField(
+            ChangeField.UPDATED.getName(), SortField.Type.LONG, true),
+          new SortField(
+            ChangeField.LEGACY_ID.getName(), SortField.Type.INT, true));
+    }
+  }
+
   private class QuerySource implements ChangeDataSource {
     private final List<SubIndex> indexes;
     private final Query query;
+    private final int start;
     private final int limit;
-    private final boolean reverse;
+    private final Sort sort;
 
-    private QuerySource(List<SubIndex> indexes, Query query, int limit,
-        boolean reverse) {
+    private QuerySource(List<SubIndex> indexes, Query query, int start,
+        int limit, Sort sort) {
       this.indexes = indexes;
       this.query = query;
+      this.start = start;
       this.limit = limit;
-      this.reverse = reverse;
+      this.sort = sort;
     }
 
     @Override
@@ -348,24 +375,19 @@
     @Override
     public ResultSet<ChangeData> read() throws OrmException {
       IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
-      Sort sort = new Sort(
-          new SortField(
-              ChangeField.SORTKEY.getName(),
-              SortField.Type.LONG,
-              // Standard order is descending by sort key, unless reversed due
-              // to a sortkey_before predicate.
-              !reverse));
       try {
+        int realLimit = start + limit;
         TopDocs[] hits = new TopDocs[indexes.size()];
         for (int i = 0; i < indexes.size(); i++) {
           searchers[i] = indexes.get(i).acquire();
-          hits[i] = searchers[i].search(query, limit, sort);
+          hits[i] = searchers[i].search(query, realLimit, sort);
         }
-        TopDocs docs = TopDocs.merge(sort, limit, hits);
+        TopDocs docs = TopDocs.merge(sort, realLimit, hits);
 
         List<ChangeData> result =
             Lists.newArrayListWithCapacity(docs.scoreDocs.length);
-        for (ScoreDoc sd : docs.scoreDocs) {
+        for (int i = start; i < docs.scoreDocs.length; i++) {
+          ScoreDoc sd = docs.scoreDocs[i];
           Document doc = searchers[sd.shardIndex].doc(sd.doc, FIELDS);
           result.add(toChangeData(doc));
         }
@@ -456,9 +478,17 @@
         doc.add(new LongField(name, (Long) value, store));
       }
     } else if (type == FieldType.TIMESTAMP) {
-      for (Object value : values.getValues()) {
-        int t = QueryBuilder.toIndexTime((Timestamp) value);
-        doc.add(new IntField(name, t, store));
+      @SuppressWarnings("deprecation")
+      boolean legacy = values.getField() == ChangeField.LEGACY_UPDATED;
+      if (legacy) {
+        for (Object value : values.getValues()) {
+          int t = queryBuilder.toIndexTimeInMinutes((Timestamp) value);
+          doc.add(new IntField(name, (int) t, store));
+        }
+      } else {
+        for (Object value : values.getValues()) {
+          doc.add(new LongField(name, ((Timestamp) value).getTime(), store));
+        }
       }
     } else if (type == FieldType.EXACT
         || type == FieldType.PREFIX) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
index faa9fe0..3221b8a 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -45,7 +45,7 @@
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.NumericUtils;
 
-import java.sql.Timestamp;
+import java.util.Date;
 import java.util.List;
 
 public class QueryBuilder {
@@ -179,28 +179,46 @@
         false, false);
   }
 
+  @SuppressWarnings("deprecation")
   private Query timestampQuery(IndexPredicate<ChangeData> p)
       throws QueryParseException {
     if (p instanceof TimestampRangePredicate) {
       TimestampRangePredicate<ChangeData> r =
           (TimestampRangePredicate<ChangeData>) p;
-      return NumericRangeQuery.newIntRange(
-          r.getField().getName(),
-          toIndexTime(r.getMinTimestamp()),
-          toIndexTime(r.getMaxTimestamp()),
-          true, true);
+      if (r.getField() == ChangeField.LEGACY_UPDATED) {
+        return NumericRangeQuery.newIntRange(
+            r.getField().getName(),
+            toIndexTimeInMinutes(r.getMinTimestamp()),
+            toIndexTimeInMinutes(r.getMaxTimestamp()),
+            true, true);
+      } else {
+        return NumericRangeQuery.newLongRange(
+            r.getField().getName(),
+            r.getMinTimestamp().getTime(),
+            r.getMaxTimestamp().getTime(),
+            true, true);
+      }
     }
     throw new QueryParseException("not a timestamp: " + p);
   }
 
+  @SuppressWarnings("deprecation")
   private Query notTimestamp(TimestampRangePredicate<ChangeData> r)
       throws QueryParseException {
     if (r.getMinTimestamp().getTime() == 0) {
-      return NumericRangeQuery.newIntRange(
-          r.getField().getName(),
-          toIndexTime(r.getMaxTimestamp()),
-          null,
-          true, true);
+      if (r.getField() == ChangeField.LEGACY_UPDATED) {
+        return NumericRangeQuery.newIntRange(
+            r.getField().getName(),
+            toIndexTimeInMinutes(r.getMaxTimestamp()),
+            null,
+            true, true);
+      } else {
+        return NumericRangeQuery.newLongRange(
+            r.getField().getName(),
+            r.getMaxTimestamp().getTime(),
+            null,
+            true, true);
+      }
     }
     throw new QueryParseException("cannot negate: " + r);
   }
@@ -232,7 +250,7 @@
     return queryBuilder.createPhraseQuery(p.getField().getName(), p.getValue());
   }
 
-  public static int toIndexTime(Timestamp ts) {
+  public int toIndexTimeInMinutes(Date ts) {
     return (int) (ts.getTime() / 60000);
   }
 
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 aba492d..9d10e66 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
@@ -71,8 +71,7 @@
 import com.google.gerrit.sshd.SshHostKeyModule;
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
-import com.google.gerrit.sshd.commands.MasterCommandModule;
-import com.google.gerrit.sshd.commands.SlaveCommandModule;
+import com.google.gerrit.sshd.commands.DefaultCommandModule;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -155,6 +154,10 @@
     this.serverStarted = serverStarted;
   }
 
+  public void setEnableHttpd(boolean enable) {
+    httpd = enable;
+  }
+
   @Override
   public int run() throws Exception {
     if (doInit) {
@@ -378,11 +381,8 @@
     if (!test) {
       modules.add(new SshHostKeyModule());
     }
-    if (slave) {
-      modules.add(new SlaveCommandModule());
-    } else {
-      modules.add(new MasterCommandModule());
-    }
+    modules.add(new DefaultCommandModule(slave));
+
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
index 5b59a25..2b0850e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Reindex.java
@@ -143,11 +143,15 @@
     sysManager.start();
 
     index = sysInjector.getInstance(IndexCollection.class).getSearchIndex();
-    index.markReady(false);
-    index.deleteAll();
-    int result = indexAll();
-    index.markReady(true);
-
+    int result = 0;
+    try {
+      index.markReady(false);
+      index.deleteAll();
+      result = indexAll();
+      index.markReady(true);
+    } catch (Exception e) {
+      throw die(e.getMessage());
+    }
     sysManager.stop();
     dbManager.stop();
     return result;
@@ -191,8 +195,26 @@
         // once, so don't worry about cache removal.
         bind(new TypeLiteral<DynamicSet<CacheRemovalListener>>() {})
             .toInstance(DynamicSet.<CacheRemovalListener> emptySet());
+        bind(new TypeLiteral<List<CommentLinkInfo>>() {})
+            .toProvider(CommentLinkProvider.class).in(SINGLETON);
+        bind(String.class).annotatedWith(CanonicalWebUrl.class)
+            .toProvider(CanonicalWebUrlProvider.class);
+        bind(IdentifiedUser.class)
+          .toProvider(Providers. <IdentifiedUser>of(null));
+        bind(CurrentUser.class).to(IdentifiedUser.class);
+        install(new AccessControlModule());
         install(new DefaultCacheFactory.Module());
+        install(new GroupModule());
+        install(new PrologModule());
+        install(AccountByEmailCacheImpl.module());
+        install(AccountCacheImpl.module());
+        install(GroupCacheImpl.module());
+        install(GroupIncludeCacheImpl.module());
+        install(ProjectCacheImpl.module());
+        install(SectionSortCache.module());
+        factory(CapabilityControl.Factory.class);
         factory(ChangeData.Factory.class);
+        factory(ProjectState.Factory.class);
 
         if (recheckMergeable) {
           install(new MergeabilityModule());
@@ -250,38 +272,20 @@
   private static class MergeabilityModule extends FactoryModule {
     @Override
     public void configure() {
-      factory(ProjectState.Factory.class);
-      bind(new TypeLiteral<List<CommentLinkInfo>>() {})
-          .toProvider(CommentLinkProvider.class).in(SINGLETON);
-      bind(IdentifiedUser.class).toProvider(Providers.<IdentifiedUser>of(null));
-      bind(CurrentUser.class).to(IdentifiedUser.class);
-      bind(String.class).annotatedWith(CanonicalWebUrl.class)
-          .toProvider(CanonicalWebUrlProvider.class);
-
       factory(PatchSetInserter.Factory.class);
       bind(ChangeHooks.class).to(DisabledChangeHooks.class);
       bind(ReplacePatchSetSender.Factory.class).toProvider(
           Providers.<ReplacePatchSetSender.Factory>of(null));
 
-      factory(CapabilityControl.Factory.class);
       factory(MergeUtil.Factory.class);
       DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
       DynamicSet.setOf(binder(), CommitValidationListener.class);
       factory(CommitValidators.Factory.class);
 
-      install(AccountCacheImpl.module());
-      install(AccountByEmailCacheImpl.module());
       install(ChangeKindCache.module());
-      install(GroupCacheImpl.module());
-      install(GroupIncludeCacheImpl.module());
-      install(ProjectCacheImpl.module());
-      install(SectionSortCache.module());
 
-      install(new AccessControlModule());
       install(new GitModule());
-      install(new GroupModule());
       install(new NoteDbModule());
-      install(new PrologModule());
     }
 
     @Provides
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
index b4d3ae4..9966fda 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -14,47 +14,15 @@
 
 package com.google.gerrit.pgm.init;
 
-import com.google.common.collect.Sets;
 import com.google.gerrit.lucene.LuceneChangeIndex;
-import com.google.gerrit.lucene.LuceneIndexModule;
 import com.google.gerrit.pgm.util.ConsoleUI;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.config.TrackingFooter;
-import com.google.gerrit.server.config.TrackingFooters;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.ChangeSchemas;
 import com.google.gerrit.server.index.IndexModule.IndexType;
-import com.google.gerrit.server.patch.IntraLineDiff;
-import com.google.gerrit.server.patch.IntraLineDiffKey;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListKey;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Guice;
 import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Provides;
-import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Repository;
-
 import java.io.IOException;
-import java.util.Collections;
-import java.util.SortedSet;
 
 /** Initialize the {@code index} configuration section. */
 @Singleton
@@ -79,105 +47,22 @@
     ui.header("Index");
 
     IndexType type = index.select("Type", "type", IndexType.LUCENE);
+    if (type == IndexType.SOLR) {
+      index.string("Solr Index URL", "url", "localhost:9983");
+    }
     if (site.isNew && type == IndexType.LUCENE) {
-      createLuceneIndex();
+      LuceneChangeIndex.setReady(
+          site, ChangeSchemas.getLatest().getVersion(), true);
     } else {
-      ui.message("The index must be built before starting Gerrit:\n"
-        + "  java -jar gerrit.war reindex -d site_path\n");
+      final String message = String.format(
+        "\nThe index must be %sbuilt before starting Gerrit:\n"
+        + "  java -jar gerrit.war reindex -d site_path\n",
+        site.isNew ? "" : "re");
+      ui.message(message);
       initFlags.autoStart = false;
     }
   }
 
-  private void createLuceneIndex() throws IOException {
-    Injector injector = Guice.createInjector(
-      new LuceneIndexModule(ChangeSchemas.getLatest().getVersion(), 0, null),
-      new MockIndexSupportModule());
-
-    ChangeIndex index = injector.getInstance(LuceneChangeIndex.class);
-    index.markReady(true);
-    index.close();
-  }
-
-  private class MockIndexSupportModule extends FactoryModule {
-    @Override
-    protected void configure() {
-      bind(SitePaths.class).toInstance(site);
-      factory(ChangeData.Factory.class);
-    }
-
-    @Provides @GerritServerConfig Config getConfig() {
-      return new Config();
-    }
-
-    @Provides TrackingFooters newTrackingFooters() {
-      return new TrackingFooters(Collections.<TrackingFooter> emptyList());
-    }
-
-    @Provides ReviewDb getReviewDb() {
-      throw new ProvisionException("database not initialized");
-    }
-
-    @Provides SchemaFactory<ReviewDb> getSchemaFactory() {
-      return new SchemaFactory<ReviewDb>() {
-        @Override
-        public ReviewDb open() throws OrmException {
-          return getReviewDb();
-        }
-      };
-    }
-
-    @Provides GitRepositoryManager getGitRepositoryManager() {
-      return new GitRepositoryManager() {
-        @Override
-        public Repository openRepository(Project.NameKey name)
-            throws RepositoryNotFoundException{
-          throw new RepositoryNotFoundException(name.get());
-        }
-
-        @Override
-        public Repository createRepository(Project.NameKey name)
-            throws RepositoryNotFoundException {
-          throw new RepositoryNotFoundException(name.get());
-        }
-
-        @Override
-        public SortedSet<Project.NameKey> list() {
-          return Sets.newTreeSet();
-        }
-
-        @Override
-        public String getProjectDescription(Project.NameKey name) {
-          return null;
-        }
-
-        @Override
-        public void setProjectDescription(NameKey name, String description) {
-        }
-      };
-    }
-
-    @Provides PatchListCache newPatchListCache() {
-      return new PatchListCache() {
-        @Override
-        public PatchList get(PatchListKey key)
-            throws PatchListNotAvailableException {
-          throw new PatchListNotAvailableException("new site, no changes");
-        }
-
-        @Override
-        public PatchList get(Change change, PatchSet patchSet)
-            throws PatchListNotAvailableException {
-          throw new PatchListNotAvailableException("new site, no changes");
-        }
-
-        @Override
-        public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key) {
-          return null;
-        }
-      };
-    }
-  }
-
   @Override
   public void postRun() throws Exception {
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
index 6ab7395..0614b53 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.pgm.util;
 
-import com.google.common.primitives.Longs;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.schema.DataSourceProvider;
@@ -68,7 +67,7 @@
         @Override
         public int compare(File a, File b) {
           // Sort by reverse last-modified time so newer JARs are first.
-          int cmp = Longs.compare(b.lastModified(), a.lastModified());
+          int cmp = Long.compare(b.lastModified(), a.lastModified());
           if (cmp != 0) {
             return cmp;
           }
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/NativeString.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/NativeString.java
index fe65857..13c8b1d 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/NativeString.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/rpc/NativeString.java
@@ -25,7 +25,7 @@
   /*-{ return $wnd.Gerrit.JsonString }-*/;
 
   public final native String asString()
-  /*-{ return this.get(); }-*/;
+  /*-{ return this; }-*/;
 
   public static final
   AsyncCallback<NativeString> unwrap(final AsyncCallback<String> cb) {
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 e57dc7f..939be28 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
@@ -86,7 +86,7 @@
    * We overrun approximately 4,083 years later, so ~6092.
    */
   @VisibleForTesting
-  public static final long SORT_KEY_EPOCH_MINS =
+  private static final long SORT_KEY_EPOCH_MINS =
       MINUTES.convert(1222819200L, SECONDS);
 
   private static final Object uuidLock = new Object();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 7d3c06e..213ecd1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -59,7 +59,7 @@
   @Override
   public AccountResource parse(TopLevelResource root, IdString id)
       throws ResourceNotFoundException, AuthException, OrmException {
-    IdentifiedUser user = _parse(id.get());
+    IdentifiedUser user = parseId(id.get());
     if (user == null) {
       throw new ResourceNotFoundException(id);
     } else if (!accountControlFactory.get().canSee(user.getAccount())) {
@@ -81,18 +81,33 @@
    */
   public IdentifiedUser parse(String id) throws AuthException,
       UnprocessableEntityException, OrmException {
-    IdentifiedUser user = _parse(id);
+    IdentifiedUser user = parseId(id);
     if (user == null) {
       throw new UnprocessableEntityException(String.format(
           "Account Not Found: %s", id));
+    } else if (!accountControlFactory.get().canSee(user.getAccount())) {
+      throw new UnprocessableEntityException(String.format(
+          "Account Not Found: %s", id));
     }
     return user;
   }
 
-  private IdentifiedUser _parse(String id) throws AuthException, OrmException {
-    CurrentUser user = self.get();
-
+  /**
+   * Parses an account ID and returns the user without making any permission
+   * check whether the current user can see the account.
+   *
+   * @param id ID of the account, can be a string of the format
+   *        "Full Name <email@example.com>", just the email address, a full name
+   *        if it is unique, an account ID, a user name or 'self' for the
+   *        calling user
+   * @return the user, null if no user is found for the given account ID
+   * @throws AuthException thrown if 'self' is used as account ID and the
+   *         current user is not authenticated
+   * @throws OrmException
+   */
+  public IdentifiedUser parseId(String id) throws AuthException, OrmException {
     if (id.equals("self")) {
+      CurrentUser user = self.get();
       if (user.isIdentifiedUser()) {
         return (IdentifiedUser) user;
       } else if (user instanceof AnonymousUser) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
index 0d672d1..4d73fe1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AddSshKey.Input;
 import com.google.gerrit.server.account.GetSshKeys.SshKeyInfo;
 import com.google.gerrit.server.ssh.SshKeyCache;
@@ -61,6 +62,11 @@
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to add SSH keys");
     }
+    return apply(rsrc.getUser(), input);
+  }
+
+  public Response<SshKeyInfo> apply(IdentifiedUser user, Input input)
+      throws BadRequestException, OrmException, IOException {
     if (input == null) {
       input = new Input();
     }
@@ -70,7 +76,7 @@
 
     int max = 0;
     for (AccountSshKey k : dbProvider.get().accountSshKeys()
-        .byAccount(rsrc.getUser().getAccountId())) {
+        .byAccount(user.getAccountId())) {
       max = Math.max(max, k.getKey().get());
     }
 
@@ -85,9 +91,9 @@
     try {
       AccountSshKey sshKey =
           sshKeyCache.create(new AccountSshKey.Id(
-              rsrc.getUser().getAccountId(), max + 1), sshPublicKey);
+              user.getAccountId(), max + 1), sshPublicKey);
       dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
-      sshKeyCache.evict(rsrc.getUser().getUserName());
+      sshKeyCache.evict(user.getUserName());
       return Response.<SshKeyInfo>created(new SshKeyInfo(sshKey));
     } catch (InvalidSshKeyException e) {
       throw new BadRequestException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index c5ccaf0..beef976 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -204,7 +204,11 @@
 
   /** True if the user has this permission. Works only for non labels. */
   public boolean canPerform(String permissionName) {
-    return !access(permissionName).isEmpty();
+    if (GlobalCapability.ADMINISTRATE_SERVER.equals(permissionName)) {
+      return canAdministrateServer();
+    } else {
+      return !access(permissionName).isEmpty();
+    }
   }
 
   /** The range of permitted values associated with a label permission. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index 3b03c3a..b657281 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -41,6 +41,7 @@
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import java.util.Collections;
@@ -64,7 +65,7 @@
   }
 
   private final ReviewDb db;
-  private final IdentifiedUser currentUser;
+  private final Provider<IdentifiedUser> currentUser;
   private final GroupsCollection groupsCollection;
   private final SshKeyCache sshKeyCache;
   private final AccountCache accountCache;
@@ -73,7 +74,7 @@
   private final String username;
 
   @Inject
-  CreateAccount(ReviewDb db, IdentifiedUser currentUser,
+  CreateAccount(ReviewDb db, Provider<IdentifiedUser> currentUser,
       GroupsCollection groupsCollection, SshKeyCache sshKeyCache,
       AccountCache accountCache, AccountByEmailCache byEmailCache,
       AccountInfo.Loader.Factory infoLoader,
@@ -164,7 +165,7 @@
           new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
       db.accountGroupMembersAudit().insert(Collections.singleton(
           new AccountGroupMemberAudit(
-              m, currentUser.getAccountId(), TimeUtil.nowTs())));
+              m, currentUser.get().getAccountId(), TimeUtil.nowTs())));
       db.accountGroupMembers().insert(Collections.singleton(m));
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
index 675ab96..b9ef19e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.CreateEmail.Input;
 import com.google.gerrit.server.account.GetEmails.EmailInfo;
 import com.google.gerrit.server.config.AuthConfig;
@@ -87,36 +88,43 @@
       throw new AuthException("not allowed to add email address");
     }
 
-    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
-      throw new MethodNotAllowedException("realm does not allow adding emails");
-    }
-
     if (input == null) {
       input = new Input();
     }
 
-    if (input.email != null && !email.equals(input.email)) {
-      throw new BadRequestException("email address must match URL");
-    }
-
     if (input.noConfirmation
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("must be administrator to use no_confirmation");
     }
 
+    return apply(rsrc.getUser(), input);
+  }
+
+  public Response<EmailInfo> apply(IdentifiedUser user, Input input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+      ResourceNotFoundException, OrmException, EmailException,
+      MethodNotAllowedException {
+    if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
+      throw new MethodNotAllowedException("realm does not allow adding emails");
+    }
+
+    if (input.email != null && !email.equals(input.email)) {
+      throw new BadRequestException("email address must match URL");
+    }
+
     EmailInfo info = new EmailInfo();
     info.email = email;
     if (input.noConfirmation
         || authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
       try {
-        accountManager.link(rsrc.getUser().getAccountId(),
+        accountManager.link(user.getAccountId(),
             AuthRequest.forEmail(email));
       } catch (AccountException e) {
         throw new ResourceConflictException(e.getMessage());
       }
       if (input.preferred) {
         putPreferredProvider.get().apply(
-            new AccountResource.Email(rsrc.getUser(), email),
+            new AccountResource.Email(user, email),
             null);
         info.preferred = true;
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index b38c49b..5e0597b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.DeleteEmail.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -55,18 +56,24 @@
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to delete email address");
     }
+    return apply(rsrc.getUser(), rsrc.getEmail());
+  }
+
+  public Response<?> apply(IdentifiedUser user, String email)
+      throws ResourceNotFoundException, ResourceConflictException,
+      MethodNotAllowedException, OrmException {
     if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) {
       throw new MethodNotAllowedException("realm does not allow deleting emails");
     }
     AccountExternalId.Key key = new AccountExternalId.Key(
-        AccountExternalId.SCHEME_MAILTO, rsrc.getEmail());
+        AccountExternalId.SCHEME_MAILTO, email);
     AccountExternalId extId = dbProvider.get().accountExternalIds().get(key);
     if (extId == null) {
-      throw new ResourceNotFoundException(rsrc.getEmail());
+      throw new ResourceNotFoundException(email);
     }
     try {
-      accountManager.unlink(rsrc.getUser().getAccountId(),
-          AuthRequest.forEmail(rsrc.getEmail()));
+      accountManager.unlink(user.getAccountId(),
+          AuthRequest.forEmail(email));
     } catch (AccountException e) {
       throw new ResourceConflictException(e.getMessage());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
index f198b77..8c878d75 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -45,10 +46,13 @@
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to get SSH keys");
     }
+    return apply(rsrc.getUser());
+  }
 
+  public List<SshKeyInfo> apply(IdentifiedUser user) throws OrmException {
     List<SshKeyInfo> sshKeys = Lists.newArrayList();
     for (AccountSshKey sshKey : dbProvider.get().accountSshKeys()
-        .byAccount(rsrc.getUser().getAccountId()).toList()) {
+        .byAccount(user.getAccountId()).toList()) {
       sshKeys.add(new SshKeyInfo(sshKey));
     }
     return sshKeys;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
index 0601b8d..f7061e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.PutHttpPassword.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -94,21 +95,24 @@
       }
       newPassword = input.httpPassword;
     }
+    return apply(rsrc.getUser(), newPassword);
+  }
 
-    if (rsrc.getUser().getUserName() == null) {
+  public Response<String> apply(IdentifiedUser user, String newPassword)
+      throws ResourceNotFoundException, ResourceConflictException, OrmException {
+    if (user.getUserName() == null) {
       throw new ResourceConflictException("username must be set");
     }
 
     AccountExternalId id = dbProvider.get().accountExternalIds()
         .get(new AccountExternalId.Key(
-            SCHEME_USERNAME,
-            rsrc.getUser().getUserName()));
+            SCHEME_USERNAME, user.getUserName()));
     if (id == null) {
       throw new ResourceNotFoundException();
     }
     id.setPassword(newPassword);
     dbProvider.get().accountExternalIds().update(Collections.singleton(id));
-    accountCache.evict(rsrc.getUser().getAccountId());
+    accountCache.evict(user.getAccountId());
 
     return Strings.isNullOrEmpty(newPassword)
         ? Response.<String>none()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
index 50497c7..6f3c032 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.PutName.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -60,7 +61,11 @@
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to change name");
     }
+    return apply(rsrc.getUser(), input);
+  }
 
+  public Response<String> apply(IdentifiedUser user, Input input)
+      throws MethodNotAllowedException, ResourceNotFoundException, OrmException {
     if (!realm.allowsEdit(FieldName.FULL_NAME)) {
       throw new MethodNotAllowedException("realm does not allow editing name");
     }
@@ -69,7 +74,7 @@
       input = new Input();
     }
 
-    Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+    Account a = dbProvider.get().accounts().get(user.getAccountId());
     if (a == null) {
       throw new ResourceNotFoundException("account not found");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
index d81f361..8fc2e6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.PutPreferred.Input;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -52,15 +53,19 @@
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("not allowed to set preferred email address");
     }
+    return apply(rsrc.getUser(), rsrc.getEmail());
+  }
 
-    Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId());
+  public Response<String> apply(IdentifiedUser user, String email)
+      throws ResourceNotFoundException, OrmException {
+    Account a = dbProvider.get().accounts().get(user.getAccountId());
     if (a == null) {
       throw new ResourceNotFoundException("account not found");
     }
-    if (rsrc.getEmail().equals(a.getPreferredEmail())) {
+    if (email.equals(a.getPreferredEmail())) {
       return Response.ok("");
     }
-    a.setPreferredEmail(rsrc.getEmail());
+    a.setPreferredEmail(email);
     dbProvider.get().accounts().update(Collections.singleton(a));
     byIdCache.evict(a.getId());
     return Response.created("");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
index 5578c3f..69acd1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -55,16 +56,20 @@
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new ResourceNotFoundException();
     }
+    return parse(rsrc.getUser(), id);
+  }
 
+  public AccountResource.SshKey parse(IdentifiedUser user, IdString id)
+      throws ResourceNotFoundException, OrmException {
     try {
       int seq = Integer.parseInt(id.get(), 10);
       AccountSshKey sshKey =
           dbProvider.get().accountSshKeys()
-              .get(new AccountSshKey.Id(rsrc.getUser().getAccountId(), seq));
+              .get(new AccountSshKey.Id(user.getAccountId(), seq));
       if (sshKey == null) {
         throw new ResourceNotFoundException(id);
       }
-      return new AccountResource.SshKey(rsrc.getUser(), sshKey);
+      return new AccountResource.SshKey(user, sshKey);
     } catch (NumberFormatException e) {
       throw new ResourceNotFoundException(id);
     }
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 0817250..0bcfd65 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
@@ -170,4 +170,14 @@
       throw new RestApiException("Cannot retrieve change", e);
     }
   }
+
+  @Override
+  public ChangeInfo get() throws RestApiException {
+    return get(EnumSet.allOf(ListChangesOption.class));
+  }
+
+  @Override
+  public ChangeInfo info() throws RestApiException {
+    return get(EnumSet.noneOf(ListChangesOption.class));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
index 1459b03..4faae5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeInfoMapper.java
@@ -118,9 +118,11 @@
       lo.value = li.value;
       lo.optional = li.optional;
       lo.values = li.values;
-      lo.all = Lists.newArrayListWithExpectedSize(li.all.size());
-      for (ChangeJson.ApprovalInfo ai : li.all) {
-        lo.all.add(fromApprovalInfo(ai));
+      if (li.all != null) {
+        lo.all = Lists.newArrayListWithExpectedSize(li.all.size());
+        for (ChangeJson.ApprovalInfo ai : li.all) {
+          lo.all.add(fromApprovalInfo(ai));
+        }
       }
       r.put(e.getKey(), lo);
     }
@@ -141,6 +143,9 @@
 
   private static AccountInfo fromAcountInfo(
       com.google.gerrit.server.account.AccountInfo i) {
+    if (i == null) {
+      return null;
+    }
     AccountInfo ai = new AccountInfo();
     fromAccount(i, ai);
     return ai;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index a42827a..33d161c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -77,6 +77,8 @@
       throw new AuthException("abandon not permitted");
     } else if (!change.getStatus().isOpen()) {
       throw new ResourceConflictException("change is " + status(change));
+    } else if (change.getStatus() == Change.Status.DRAFT) {
+      throw new ResourceConflictException("draft changes cannot be abandoned");
     }
 
     ChangeMessage message;
@@ -133,6 +135,7 @@
       .setLabel("Abandon")
       .setTitle("Abandon the change")
       .setVisible(resource.getChange().getStatus().isOpen()
+          && resource.getChange().getStatus() != Change.Status.DRAFT
           && resource.getControl().canAbandon());
   }
 
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 5da693d..1236985 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
@@ -86,6 +86,7 @@
 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.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -226,6 +227,9 @@
   private ChangeInfo format(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
       throws OrmException {
     accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+    if (has(REVIEWED)) {
+      ensureReviewedLoaded(Collections.singleton(cd));
+    }
     ChangeInfo res = toChangeInfo(cd, limitToPsId);
     accountLoader.fill();
     return res;
@@ -349,22 +353,20 @@
   }
 
   private ChangeControl control(ChangeData cd) throws OrmException {
-    ChangeControl ctrl = cd.changeControl();
-    if (ctrl != null && ctrl.getCurrentUser() == userProvider.get()) {
-      return ctrl;
-    } else if (lastControl != null
+    if (lastControl != null
         && cd.getId().equals(lastControl.getChange().getId())) {
       return lastControl;
     }
-
+    ChangeControl ctrl;
     try {
-      Change change = cd.change();
-      if (change == null) {
-        return null;
+      if (cd.hasChangeControl()) {
+        ctrl = cd.changeControl().forUser(userProvider.get());
+      } else {
+        ctrl = projectControls.get(cd.change().getProject())
+            .controlFor(cd.change());
       }
-      ctrl = projectControls.get(change.getProject()).controlFor(change);
-    } catch (ExecutionException e) {
-      return null;
+    } catch (NoSuchChangeException | ExecutionException e) {
+      throw new OrmException(e);
     }
     lastControl = ctrl;
     return ctrl;
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 22d3e9a..45e57fa 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
@@ -135,7 +135,8 @@
             ChangeIdUtil
                 .computeChangeId(commitToCherryPick.getTree(), mergeTip,
                     commitToCherryPick.getAuthorIdent(), myIdent, message);
-        String commitMessage = ChangeIdUtil.insertId(message, computedChangeId);
+        String commitMessage =
+            ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n';
 
         RevCommit cherryPickCommit;
         ObjectInserter oi = git.newObjectInserter();
@@ -149,8 +150,7 @@
         }
 
         if (cherryPickCommit == null) {
-          throw new MergeException(
-              "Could not create a merge commit during the cherry pick");
+          throw new MergeException("Cherry pick failed");
         }
 
         Change.Key changeKey;
@@ -170,7 +170,7 @@
 
         if (destChanges.size() > 1) {
           throw new InvalidChangeOperationException("Several changes with key "
-              + changeKey + " resides on the same branch. "
+              + changeKey + " reside on the same branch. "
               + "Cannot create a new patch set.");
         } else if (destChanges.size() == 1) {
           // The change key exists on the destination branch. The cherry pick
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 a975c9c..4e45b1e 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
@@ -309,16 +309,14 @@
     }
 
     List<PatchLineComment> del = Lists.newArrayList();
-    List<PatchLineComment> ins = Lists.newArrayList();
-    List<PatchLineComment> upd = Lists.newArrayList();
+    List<PatchLineComment> ups = Lists.newArrayList();
 
     for (Map.Entry<String, List<Comment>> ent : in.entrySet()) {
       String path = ent.getKey();
       for (Comment c : ent.getValue()) {
         String parent = Url.decode(c.inReplyTo);
         PatchLineComment e = drafts.remove(Url.decode(c.id));
-        boolean create = e == null;
-        if (create) {
+        if (e == null) {
           e = new PatchLineComment(
               new PatchLineComment.Key(
                   new Patch.Key(rsrc.getPatchSet().getId(), path),
@@ -340,7 +338,7 @@
               c.range.endLine,
               c.range.endCharacter));
         }
-        (create ? ins : upd).add(e);
+        ups.add(e);
       }
     }
 
@@ -355,16 +353,14 @@
         for (PatchLineComment e : drafts.values()) {
           e.setStatus(PatchLineComment.Status.PUBLISHED);
           e.setWrittenOn(timestamp);
-          upd.add(e);
+          ups.add(e);
         }
         break;
     }
     db.get().patchComments().delete(del);
-    db.get().patchComments().insert(ins);
-    db.get().patchComments().update(upd);
-    comments.addAll(ins);
-    comments.addAll(upd);
-    return !del.isEmpty() || !ins.isEmpty() || !upd.isEmpty();
+    db.get().patchComments().upsert(ups);
+    comments.addAll(ups);
+    return !del.isEmpty() || !ups.isEmpty();
   }
 
   private Map<String, PatchLineComment> scanDraftComments(
@@ -385,8 +381,7 @@
     }
 
     List<PatchSetApproval> del = Lists.newArrayList();
-    List<PatchSetApproval> ins = Lists.newArrayList();
-    List<PatchSetApproval> upd = Lists.newArrayList();
+    List<PatchSetApproval> ups = Lists.newArrayList();
     Map<String, PatchSetApproval> current = scanLabels(rsrc, del);
 
     LabelTypes labelTypes = rsrc.getControl().getLabelTypes();
@@ -412,7 +407,7 @@
       } else if (c != null && c.getValue() != ent.getValue()) {
         c.setValue(ent.getValue());
         c.setGranted(timestamp);
-        upd.add(c);
+        ups.add(c);
         addLabelDelta(normName, c.getValue());
         categories.put(normName, c.getValue());
         update.putApproval(ent.getKey(), ent.getValue());
@@ -425,24 +420,23 @@
                 lt.getLabelId()),
             ent.getValue(), TimeUtil.nowTs());
         c.setGranted(timestamp);
-        ins.add(c);
+        ups.add(c);
         addLabelDelta(normName, c.getValue());
         categories.put(normName, c.getValue());
         update.putApproval(ent.getKey(), ent.getValue());
       }
     }
 
-    forceCallerAsReviewer(rsrc, current, ins, upd, del);
+    forceCallerAsReviewer(rsrc, current, ups, del);
     db.get().patchSetApprovals().delete(del);
-    db.get().patchSetApprovals().insert(ins);
-    db.get().patchSetApprovals().update(upd);
-    return !del.isEmpty() || !ins.isEmpty() || !upd.isEmpty();
+    db.get().patchSetApprovals().upsert(ups);
+    return !del.isEmpty() || !ups.isEmpty();
   }
 
   private void forceCallerAsReviewer(RevisionResource rsrc,
-      Map<String, PatchSetApproval> current, List<PatchSetApproval> ins,
-      List<PatchSetApproval> upd, List<PatchSetApproval> del) {
-    if (current.isEmpty() && ins.isEmpty() && upd.isEmpty()) {
+      Map<String, PatchSetApproval> current, List<PatchSetApproval> ups,
+      List<PatchSetApproval> del) {
+    if (current.isEmpty() && ups.isEmpty()) {
       // TODO Find another way to link reviewers to changes.
       if (del.isEmpty()) {
         // If no existing label is being set to 0, hack in the caller
@@ -454,7 +448,7 @@
                 .getLabelId()),
             (short) 0, TimeUtil.nowTs());
         c.setGranted(timestamp);
-        ins.add(c);
+        ups.add(c);
       } else {
         // Pick a random label that is about to be deleted and keep it.
         Iterator<PatchSetApproval> i = del.iterator();
@@ -462,7 +456,7 @@
         c.setValue((short) 0);
         c.setGranted(timestamp);
         i.remove();
-        upd.add(c);
+        ups.add(c);
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index 66af2f1..b0fe0b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -292,7 +292,7 @@
     rebasedCommit = revWalk.parseCommit(newId);
 
     final ChangeControl changeControl =
-        changeControlFactory.validateFor(change.getId(), uploader);
+        changeControlFactory.validateFor(change, uploader);
 
     PatchSetInserter patchSetInserter = patchSetInserterFactory
         .create(git, revWalk, changeControl, rebasedCommit)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
index a72297d..56a5ae1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
@@ -22,8 +22,10 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
 
@@ -118,29 +120,30 @@
           }
         }
 
-      } catch (IOException e) {
-        throw new MergeException("Cannot merge " + n.name(), e);
-      } catch (OrmException e) {
+      } catch (NoSuchChangeException | IOException | OrmException e) {
         throw new MergeException("Cannot merge " + n.name(), e);
       }
     }
     return newMergeTip;
   }
 
-  private CodeReviewCommit writeCherryPickCommit(final CodeReviewCommit mergeTip, final CodeReviewCommit n)
-      throws IOException, OrmException {
+  private CodeReviewCommit writeCherryPickCommit(CodeReviewCommit mergeTip,
+      CodeReviewCommit n) throws IOException, OrmException,
+      NoSuchChangeException {
 
     args.rw.parseBody(n);
 
     final PatchSetApproval submitAudit = args.mergeUtil.getSubmitter(n);
 
+    IdentifiedUser cherryPickUser;
     PersonIdent cherryPickCommitterIdent;
     if (submitAudit != null) {
-      cherryPickCommitterIdent =
-          args.identifiedUserFactory.create(submitAudit.getAccountId())
-              .newCommitterIdent(submitAudit.getGranted(),
-                  args.myIdent.getTimeZone());
+      cherryPickUser =
+          args.identifiedUserFactory.create(submitAudit.getAccountId());
+      cherryPickCommitterIdent = cherryPickUser.newCommitterIdent(
+          submitAudit.getGranted(), args.myIdent.getTimeZone());
     } else {
+      cherryPickUser = args.identifiedUserFactory.create(n.change().getOwner());
       cherryPickCommitterIdent = args.myIdent;
     }
 
@@ -156,7 +159,7 @@
     }
 
     PatchSet.Id id =
-        ChangeUtil.nextPatchSetId(args.repo, n.getChange().currentPatchSetId());
+        ChangeUtil.nextPatchSetId(args.repo, n.change().currentPatchSetId());
     final PatchSet ps = new PatchSet(id);
     ps.setCreatedOn(TimeUtil.nowTs());
     ps.setUploader(submitAudit.getAccountId());
@@ -164,17 +167,17 @@
 
     final RefUpdate ru;
 
-    args.db.changes().beginTransaction(n.getChange().getId());
+    args.db.changes().beginTransaction(n.change().getId());
     try {
       insertAncestors(args.db, ps.getId(), newCommit);
       args.db.patchSets().insert(Collections.singleton(ps));
-      n.getChange()
+      n.change()
           .setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
-      args.db.changes().update(Collections.singletonList(n.getChange()));
+      args.db.changes().update(Collections.singletonList(n.change()));
 
       final List<PatchSetApproval> approvals = Lists.newArrayList();
       for (PatchSetApproval a
-          : args.approvalsUtil.byPatchSet(args.db, n.notes, n.patchsetId)) {
+          : args.approvalsUtil.byPatchSet(args.db, n.notes(), n.patchsetId)) {
         approvals.add(new PatchSetApproval(ps.getId(), a));
       }
       // TODO(dborowitz): This doesn't copy labels in the notedb. We should
@@ -188,7 +191,7 @@
       ru.disableRefLog();
       if (ru.update(args.rw) != RefUpdate.Result.NEW) {
         throw new IOException(String.format(
-            "Failed to create ref %s in %s: %s", ps.getRefName(), n.getChange()
+            "Failed to create ref %s in %s: %s", ps.getRefName(), n.change()
                 .getDest().getParentKey().get(), ru.getResult()));
       }
 
@@ -197,10 +200,12 @@
       args.db.rollback();
     }
 
-    gitRefUpdated.fire(n.getChange().getProject(), ru);
+    gitRefUpdated.fire(n.change().getProject(), ru);
 
     newCommit.copyFrom(n);
     newCommit.statusCode = CommitMergeStatus.CLEAN_PICK;
+    newCommit.control =
+        args.changeControlFactory.controlFor(n.change(), cherryPickUser);
     newCommits.put(newCommit.patchsetId.getParentKey(), newCommit);
     setRefLogIdent(submitAudit);
     return newCommit;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 789be78..edd62af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ChangeControl;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
@@ -40,8 +41,8 @@
    */
   PatchSet.Id patchsetId;
 
-  /** Change info loaded from notes. */
-  public ChangeNotes notes;
+  /** Change control for the change owner. */
+  ChangeControl control;
 
   /**
    * Ordinal position of this commit within the submit queue.
@@ -64,15 +65,19 @@
     super(id);
   }
 
+  public ChangeNotes notes() {
+    return control.getNotes();
+  }
+
   void copyFrom(final CodeReviewCommit src) {
+    control = src.control;
     patchsetId = src.patchsetId;
-    notes = src.notes;
     originalOrder = src.originalOrder;
     statusCode = src.statusCode;
     missing = src.missing;
   }
 
-  Change getChange() {
-    return notes.getChange();
+  Change change() {
+    return control.getChange();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index b146f48..d187708 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -279,8 +279,8 @@
       for (final CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) {
         final Capable capable = isSubmitStillPossible(commit);
         if (capable != Capable.OK) {
-          sendMergeFail(commit.notes,
-              message(commit.getChange(), capable.getMessage()), false);
+          sendMergeFail(commit.notes(),
+              message(commit.change(), capable.getMessage()), false);
         }
       }
     } catch (NoSuchProjectException noProject) {
@@ -327,7 +327,12 @@
     }
 
     for (CodeReviewCommit missingCommit : commit.missing) {
-      loadChangeInfo(missingCommit);
+      try {
+        loadChangeInfo(missingCommit);
+      } catch (NoSuchChangeException | OrmException e) {
+        log.error("Cannot check if missing commits can be submitted", e);
+        return false;
+      }
 
       if (missingCommit.patchsetId == null) {
         // The commit doesn't have a patch set, so it cannot be
@@ -336,7 +341,7 @@
         return false;
       }
 
-      if (!missingCommit.getChange().currentPatchSetId().equals(
+      if (!missingCommit.change().currentPatchSetId().equals(
           missingCommit.patchsetId)) {
         // If the missing commit is not the current patch set,
         // the change must be rebased to use the proper parent.
@@ -519,7 +524,12 @@
         continue;
       }
 
-      commit.notes = notesFactory.create(chg);
+      try {
+        commit.control = changeControlFactory.controlFor(chg,
+            identifiedUserFactory.create(chg.getOwner()));
+      } catch (NoSuchChangeException e) {
+        throw new MergeException("Failed to validate changes", e);
+      }
       commit.patchsetId = ps.getId();
       commit.originalOrder = commitOrder++;
       commits.put(changeId, commit);
@@ -544,7 +554,7 @@
         }
       }
 
-      final SubmitType submitType = getSubmitType(chg, ps);
+      final SubmitType submitType = getSubmitType(commit.control, ps);
       if (submitType == null) {
         commits.put(changeId,
             CodeReviewCommit.error(CommitMergeStatus.NO_SUBMIT_TYPE));
@@ -559,21 +569,13 @@
     return toSubmit;
   }
 
-  private SubmitType getSubmitType(final Change change, final PatchSet ps) {
-    try {
-      final SubmitTypeRecord r =
-          changeControlFactory.controlFor(change,
-              identifiedUserFactory.create(change.getOwner()))
-              .getSubmitTypeRecord(db, ps);
-      if (r.status != SubmitTypeRecord.Status.OK) {
-        log.error("Failed to get submit type for " + change.getKey());
-        return null;
-      }
-      return r.type;
-    } catch (NoSuchChangeException e) {
-      log.error("Failed to get submit type for " + change.getKey(), e);
+  private SubmitType getSubmitType(ChangeControl ctl, PatchSet ps) {
+    SubmitTypeRecord r = ctl.getSubmitTypeRecord(db, ps);
+    if (r.status != SubmitTypeRecord.Status.OK) {
+      log.error("Failed to get submit type for " + ctl.getChange().getKey());
       return null;
     }
+    return r.type;
   }
 
   private void updateBranch(final SubmitStrategy strategy,
@@ -623,7 +625,7 @@
 
             Account account = null;
             PatchSetApproval submitter = approvalsUtil.getSubmitter(
-                db, mergeTip.notes, mergeTip.patchsetId);
+                db, mergeTip.notes(), mergeTip.patchsetId);
             if (submitter != null) {
               account = accountCache.get(submitter.getAccountId()).getAccount();
             }
@@ -723,7 +725,7 @@
 
   private Capable isSubmitStillPossible(final CodeReviewCommit commit) {
     final Capable capable;
-    final Change c = commit.getChange();
+    final Change c = commit.change();
     final boolean submitStillPossible = isSubmitForMissingCommitsStillPossible(commit);
     final long now = TimeUtil.nowMs();
     final long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY;
@@ -748,7 +750,7 @@
       m.append("\n");
       for (CodeReviewCommit missingCommit : commit.missing) {
         m.append("* ");
-        m.append(missingCommit.getChange().getKey().get());
+        m.append(missingCommit.change().getKey().get());
         m.append("\n");
       }
       capable = new Capable(m.toString());
@@ -767,10 +769,10 @@
           m.append("* Depends on patch set ");
           m.append(missingCommit.patchsetId.get());
           m.append(" of ");
-          m.append(missingCommit.getChange().getKey().abbreviate());
-          if (missingCommit.patchsetId.get() != missingCommit.getChange().currentPatchSetId().get()) {
+          m.append(missingCommit.change().getKey().abbreviate());
+          if (missingCommit.patchsetId.get() != missingCommit.change().currentPatchSetId().get()) {
             m.append(", however the current patch set is ");
-            m.append(missingCommit.getChange().currentPatchSetId().get());
+            m.append(missingCommit.change().currentPatchSetId().get());
           }
           m.append(".\n");
 
@@ -788,18 +790,16 @@
     return capable;
   }
 
-  private void loadChangeInfo(final CodeReviewCommit commit) {
-    if (commit.notes == null) {
-      try {
-        List<PatchSet> matches =
-            db.patchSets().byRevision(new RevId(commit.name())).toList();
-        if (matches.size() == 1) {
-          final PatchSet ps = matches.get(0);
-          commit.patchsetId = ps.getId();
-          commit.notes =
-              notesFactory.create(db.changes().get(ps.getId().getParentKey()));
-        }
-      } catch (OrmException e) {
+  private void loadChangeInfo(final CodeReviewCommit commit)
+      throws NoSuchChangeException, OrmException {
+    if (commit.control == null) {
+      List<PatchSet> matches =
+          db.patchSets().byRevision(new RevId(commit.name())).toList();
+      if (matches.size() == 1) {
+        PatchSet ps = matches.get(0);
+        commit.patchsetId = ps.getId();
+        commit.control =
+            changeControl(db.changes().get(ps.getId().getParentKey()));
       }
     }
   }
@@ -825,10 +825,10 @@
       // We must pull the patchset out of commits, because the patchset ID is
       // modified when using the cherry-pick merge strategy.
       CodeReviewCommit commit = commits.get(c.getId());
-      PatchSet.Id merged = commit.getChange().currentPatchSetId();
+      PatchSet.Id merged = commit.change().currentPatchSetId();
       c = setMergedPatchSet(c.getId(), merged);
       PatchSetApproval submitter =
-          approvalsUtil.getSubmitter(db, commit.notes, merged);
+          approvalsUtil.getSubmitter(db, commit.notes(), merged);
       addMergedMessage(submitter, msg);
 
       db.commit();
@@ -838,7 +838,7 @@
         try {
           hooks.doChangeMergedHook(c,
               accountCache.get(submitter.getAccountId()).getAccount(),
-              db.patchSets().get(commit.patchsetId), db);
+              db.patchSets().get(merged), db);
         } catch (OrmException ex) {
           log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
         }
@@ -904,9 +904,7 @@
         }
 
         try {
-          final ChangeControl control = changeControlFactory.controlFor(c,
-              identifiedUserFactory.create(c.getOwner()));
-          final MergedSender cm = mergedSenderFactory.create(control);
+          MergedSender cm = mergedSenderFactory.create(changeControl(c));
           if (from != null) {
             cm.setFrom(from.getAccountId());
           }
@@ -924,8 +922,13 @@
     }));
   }
 
+  private ChangeControl changeControl(Change c) throws NoSuchChangeException {
+    return changeControlFactory.controlFor(
+        c, identifiedUserFactory.create(c.getOwner()));
+  }
+
   private void setNew(CodeReviewCommit c, ChangeMessage msg) {
-    sendMergeFail(c.notes, msg, true);
+    sendMergeFail(c.notes(), msg, true);
   }
 
   private void setNew(Change c, ChangeMessage msg) throws OrmException {
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 09a0e68..a6acf60 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
@@ -168,7 +168,7 @@
   }
 
   PatchSetApproval getSubmitter(CodeReviewCommit c) {
-    return approvalsUtil.getSubmitter(db.get(), c.notes, c.patchsetId);
+    return approvalsUtil.getSubmitter(db.get(), c.notes(), c.patchsetId);
   }
 
   public RevCommit createCherryPickFromCommit(Repository repo,
@@ -217,10 +217,10 @@
       msgbuf.append('\n');
     }
 
-    if (!contains(footers, CHANGE_ID, n.getChange().getKey().get())) {
+    if (!contains(footers, CHANGE_ID, n.change().getKey().get())) {
       msgbuf.append(CHANGE_ID.getName());
       msgbuf.append(": ");
-      msgbuf.append(n.getChange().getKey().get());
+      msgbuf.append(n.change().getKey().get());
       msgbuf.append('\n');
     }
 
@@ -313,7 +313,7 @@
 
   private List<PatchSetApproval> safeGetApprovals(CodeReviewCommit n) {
     try {
-      return approvalsUtil.byPatchSet(db.get(), n.notes, n.patchsetId);
+      return approvalsUtil.byPatchSet(db.get(), n.notes(), n.patchsetId);
     } catch (OrmException e) {
       log.error("Can't read approval records for " + n.patchsetId, e);
       return Collections.emptyList();
@@ -587,7 +587,10 @@
     mergeCommit.setCommitter(myIdent);
     mergeCommit.setMessage(msgbuf.toString());
 
-    return (CodeReviewCommit) rw.parseCommit(commit(inserter, mergeCommit));
+    CodeReviewCommit mergeResult =
+        (CodeReviewCommit) rw.parseCommit(commit(inserter, mergeCommit));
+    mergeResult.control = n.control;
+    return mergeResult;
   }
 
   private String summarize(RevWalk rw, List<CodeReviewCommit> merged)
@@ -600,8 +603,8 @@
 
     LinkedHashSet<String> topics = new LinkedHashSet<>(4);
     for (CodeReviewCommit c : merged) {
-      if (!Strings.isNullOrEmpty(c.getChange().getTopic())) {
-        topics.add(c.getChange().getTopic());
+      if (!Strings.isNullOrEmpty(c.change().getTopic())) {
+        topics.add(c.change().getTopic());
       }
     }
 
@@ -616,7 +619,7 @@
               new Function<CodeReviewCommit, String>() {
                 @Override
                 public String apply(CodeReviewCommit in) {
-                  return in.getChange().getKey().abbreviate();
+                  return in.change().getKey().abbreviate();
                 }
               })),
           merged.size() > 5 ? ", ..." : "");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 1db7b4a..55f4ff7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -114,6 +114,7 @@
   private static final String KEY_MAX_OBJECT_SIZE_LIMIT = "maxObjectSizeLimit";
   private static final String KEY_REQUIRE_CONTRIBUTOR_AGREEMENT =
       "requireContributorAgreement";
+  private static final String KEY_CHECK_RECEIVED_OBJECTS = "checkReceivedObjects";
 
   private static final String SUBMIT = "submit";
   private static final String KEY_ACTION = "action";
@@ -157,6 +158,7 @@
   private ObjectId rulesId;
   private long maxObjectSizeLimit;
   private Map<String, Config> pluginConfigs;
+  private boolean checkReceivedObjects;
 
   public static ProjectConfig read(MetaDataUpdate update) throws IOException,
       ConfigInvalidException {
@@ -339,6 +341,13 @@
   }
 
   /**
+   * @return the checkReceivedObjects for this project, default is true.
+   */
+  public boolean getCheckReceivedObjects() {
+    return checkReceivedObjects;
+  }
+
+  /**
    * Check all GroupReferences use current group name, repairing stale ones.
    *
    * @param groupBackend cache to use when looking up group information by UUID.
@@ -408,8 +417,7 @@
     loadLabelSections(rc);
     loadCommentLinkSections(rc);
     loadPluginSections(rc);
-
-    maxObjectSizeLimit = rc.getLong(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, 0);
+    loadReceiveSection(rc);
   }
 
   private void loadAccountsSection(
@@ -696,6 +704,11 @@
     commentLinkSections = ImmutableList.copyOf(commentLinkSections);
   }
 
+  private void loadReceiveSection(Config rc) {
+    checkReceivedObjects = rc.getBoolean(RECEIVE, KEY_CHECK_RECEIVED_OBJECTS, true);
+    maxObjectSizeLimit = rc.getLong(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, 0);
+  }
+
   private void loadPluginSections(Config rc) {
     pluginConfigs = Maps.newHashMap();
     for (String plugin : rc.getSubsections(PLUGIN)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
index 01d37dc..a5b0034 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy;
 import com.google.gerrit.server.changedetail.PathConflictException;
 import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
@@ -35,14 +36,17 @@
 import java.util.Map;
 
 public class RebaseIfNecessary extends SubmitStrategy {
-
+  private final PatchSetInfoFactory patchSetInfoFactory;
   private final RebaseChange rebaseChange;
   private final Map<Change.Id, CodeReviewCommit> newCommits;
   private final PersonIdent committerIdent;
 
-  RebaseIfNecessary(final SubmitStrategy.Arguments args,
-      final RebaseChange rebaseChange, PersonIdent committerIdent) {
+  RebaseIfNecessary(SubmitStrategy.Arguments args,
+      PatchSetInfoFactory patchSetInfoFactory,
+      RebaseChange rebaseChange,
+      PersonIdent committerIdent) {
     super(args);
+    this.patchSetInfoFactory = patchSetInfoFactory;
     this.rebaseChange = rebaseChange;
     this.newCommits = new HashMap<Change.Id, CodeReviewCommit>();
     this.committerIdent = committerIdent;
@@ -82,7 +86,7 @@
                 args.mergeUtil.getSubmitter(n).getAccountId());
             final PatchSet newPatchSet =
                 rebaseChange.rebase(args.repo, args.rw, args.inserter,
-                    n.patchsetId, n.getChange(), uploader,
+                    n.patchsetId, n.change(), uploader,
                     newMergeTip, args.mergeUtil, committerIdent,
                     false, false, ValidatePolicy.NONE);
 
@@ -91,7 +95,7 @@
             // describes the change being submitted.
             List<PatchSetApproval> approvals = Lists.newArrayList();
             for (PatchSetApproval a : args.approvalsUtil.byPatchSet(
-                args.db, n.notes, n.patchsetId)) {
+                args.db, n.notes(), n.patchsetId)) {
               approvals.add(new PatchSetApproval(newPatchSet.getId(), a));
             }
             args.db.patchSetApprovals().insert(approvals);
@@ -99,22 +103,19 @@
             newMergeTip =
                 (CodeReviewCommit) args.rw.parseCommit(ObjectId
                     .fromString(newPatchSet.getRevision().get()));
+            n.change().setCurrentPatchSet(
+                patchSetInfoFactory.get(newMergeTip, newPatchSet.getId()));
             newMergeTip.copyFrom(n);
+            newMergeTip.control =
+                args.changeControlFactory.controlFor(n.change(), uploader);
             newMergeTip.patchsetId = newPatchSet.getId();
-            newMergeTip.notes = args.notesFactory.create(
-                args.db.changes().get(newPatchSet.getId().getParentKey()));
             newMergeTip.statusCode = CommitMergeStatus.CLEAN_REBASE;
             newCommits.put(newPatchSet.getId().getParentKey(), newMergeTip);
             setRefLogIdent(args.mergeUtil.getSubmitter(n));
           } catch (PathConflictException e) {
             n.statusCode = CommitMergeStatus.PATH_CONFLICT;
-          } catch (NoSuchChangeException e) {
-            throw new MergeException("Cannot rebase " + n.name(), e);
-          } catch (OrmException e) {
-            throw new MergeException("Cannot rebase " + n.name(), e);
-          } catch (IOException e) {
-            throw new MergeException("Cannot rebase " + n.name(), e);
-          } catch (InvalidChangeOperationException e) {
+          } catch (NoSuchChangeException | OrmException | IOException
+              | InvalidChangeOperationException e) {
             throw new MergeException("Cannot rebase " + n.name(), e);
           }
         }
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 29efb81..e602ccb 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
@@ -406,10 +406,12 @@
 
     this.messageSender = new ReceivePackMessageSender();
 
+    ProjectState ps = projectControl.getProjectState();
+
     rp.setAllowCreates(true);
     rp.setAllowDeletes(true);
     rp.setAllowNonFastForwards(true);
-    rp.setCheckReceivedObjects(true);
+    rp.setCheckReceivedObjects(ps.getConfig().getCheckReceivedObjects());
     rp.setRefFilter(new RefFilter() {
       @Override
       public Map<String, Ref> filter(Map<String, Ref> refs) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
index 8fc1c35..078fa9f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
@@ -22,9 +22,7 @@
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.index.ChangeIndexer;
-
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.project.ChangeControl;
 
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -52,8 +50,7 @@
     protected final IdentifiedUser.GenericFactory identifiedUserFactory;
     protected final PersonIdent myIdent;
     protected final ReviewDb db;
-    protected final ChangeNotes.Factory notesFactory;
-    protected final ChangeUpdate.Factory updateFactory;
+    protected final ChangeControl.GenericFactory changeControlFactory;
 
     protected final Repository repo;
     protected final RevWalk rw;
@@ -68,17 +65,15 @@
 
     Arguments(final IdentifiedUser.GenericFactory identifiedUserFactory,
         final PersonIdent myIdent, final ReviewDb db,
-        final ChangeNotes.Factory notesFactory,
-        final ChangeUpdate.Factory updateFactory, final Repository repo,
-        final RevWalk rw, final ObjectInserter inserter,
+        final ChangeControl.GenericFactory changeControlFactory,
+        final Repository repo, final RevWalk rw, final ObjectInserter inserter,
         final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
         final Branch.NameKey destBranch, final ApprovalsUtil approvalsUtil,
         final MergeUtil mergeUtil, final ChangeIndexer indexer) {
       this.identifiedUserFactory = identifiedUserFactory;
       this.myIdent = myIdent;
       this.db = db;
-      this.notesFactory = notesFactory;
-      this.updateFactory = updateFactory;
+      this.changeControlFactory = changeControlFactory;
 
       this.repo = repo;
       this.rw = rw;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
index 4b183b8..d818b5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
@@ -23,9 +23,8 @@
 import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
@@ -49,8 +48,7 @@
 
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
   private final PersonIdent myIdent;
-  private final ChangeNotes.Factory notesFactory;
-  private final ChangeUpdate.Factory updateFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final GitReferenceUpdated gitRefUpdated;
   private final RebaseChange rebaseChange;
@@ -63,8 +61,7 @@
   SubmitStrategyFactory(
       final IdentifiedUser.GenericFactory identifiedUserFactory,
       @GerritPersonIdent final PersonIdent myIdent,
-      final ChangeNotes.Factory notesFactory,
-      final ChangeUpdate.Factory updateFactory,
+      final ChangeControl.GenericFactory changeControlFactory,
       final PatchSetInfoFactory patchSetInfoFactory,
       final GitReferenceUpdated gitRefUpdated, final RebaseChange rebaseChange,
       final ProjectCache projectCache,
@@ -73,8 +70,7 @@
       final ChangeIndexer indexer) {
     this.identifiedUserFactory = identifiedUserFactory;
     this.myIdent = myIdent;
-    this.notesFactory = notesFactory;
-    this.updateFactory = updateFactory;
+    this.changeControlFactory = changeControlFactory;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.gitRefUpdated = gitRefUpdated;
     this.rebaseChange = rebaseChange;
@@ -92,7 +88,7 @@
     ProjectState project = getProject(destBranch);
     final SubmitStrategy.Arguments args =
         new SubmitStrategy.Arguments(identifiedUserFactory, myIdent, db,
-            notesFactory, updateFactory, repo, rw, inserter, canMergeFlag,
+            changeControlFactory, repo, rw, inserter, canMergeFlag,
             alreadyAccepted, destBranch,approvalsUtil,
             mergeUtilFactory.create(project), indexer);
     switch (submitType) {
@@ -105,7 +101,8 @@
       case MERGE_IF_NECESSARY:
         return new MergeIfNecessary(args);
       case REBASE_IF_NECESSARY:
-        return new RebaseIfNecessary(args, rebaseChange, myIdent);
+        return new RebaseIfNecessary(
+            args, patchSetInfoFactory, rebaseChange, myIdent);
       default:
         final String errorMsg = "No submit strategy for: " + submitType;
         log.error(errorMsg);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 519fc41..76998b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -125,7 +125,7 @@
           } else {
             if (!oldParent.equals(newParent)) {
               PatchSetApproval psa =
-                  approvalsUtil.getSubmitter(db, commit.notes, patchSetId);
+                  approvalsUtil.getSubmitter(db, commit.notes(), patchSetId);
               if (psa == null) {
                 throw new MergeValidationException(CommitMergeStatus.
                     SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
index 91da748..7f9fe77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
@@ -84,7 +84,7 @@
       throw new ResourceNotFoundException(id);
     }
 
-    GroupDescription.Basic group = _parse(id.get());
+    GroupDescription.Basic group = parseId(id.get());
     if (group == null) {
       throw new ResourceNotFoundException(id.get());
     }
@@ -106,7 +106,7 @@
    */
   public GroupDescription.Basic parse(String id)
       throws UnprocessableEntityException {
-    GroupDescription.Basic group = _parse(id);
+    GroupDescription.Basic group = parseId(id);
     if (group == null || !groupControlFactory.controlFor(group).isVisible()) {
       throw new UnprocessableEntityException(String.format(
           "Group Not Found: %s", id));
@@ -135,7 +135,15 @@
     return group;
   }
 
-  private GroupDescription.Basic _parse(String id) {
+  /**
+   * Parses a group ID and returns the group without making any permission
+   * check whether the current user can see the group.
+   *
+   * @param id ID of the group, can be a group UUID, a group name or a legacy
+   *        group ID
+   * @return the group, null if no group is found for the given group ID
+   */
+  public GroupDescription.Basic parseId(String id) {
     AccountGroup.UUID uuid = new AccountGroup.UUID(id);
     if (groupBackend.handles(uuid)) {
       GroupDescription.Basic d = groupBackend.get(uuid);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index 5c5e741..c4b2ae2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -57,6 +57,10 @@
     this.accountLoader = accountLoaderFactory.create(true);
   }
 
+  public void setRecursive(boolean recursive) {
+    this.recursive = recursive;
+  }
+
   @Override
   public List<AccountInfo> apply(final GroupResource resource)
       throws MethodNotAllowedException, OrmException {
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 cca9d8f..afa516f 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
@@ -115,10 +115,24 @@
         }
       };
 
+  // Same value as UPDATED, but implementations truncated to minutes.
+  @Deprecated
+  /** Last update time since January 1, 1970. */
+  public static final FieldDef<ChangeData, Timestamp> LEGACY_UPDATED =
+      new FieldDef.Single<ChangeData, Timestamp>(
+          "updated", FieldType.TIMESTAMP, true) {
+        @Override
+        public Timestamp get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return input.change().getLastUpdatedOn();
+        }
+      };
+
+
   /** Last update time since January 1, 1970. */
   public static final FieldDef<ChangeData, Timestamp> UPDATED =
       new FieldDef.Single<ChangeData, Timestamp>(
-          "updated", FieldType.TIMESTAMP, true) {
+          "updated2", FieldType.TIMESTAMP, true) {
         @Override
         public Timestamp get(ChangeData input, FillArgs args)
             throws OrmException {
@@ -152,6 +166,7 @@
    * Redundant with {@link #UPDATED} and {@link #LEGACY_ID}, but secondary index
    * implementations may not be able to search over tuples of values.
    */
+  @Deprecated
   public static final FieldDef<ChangeData, Long> SORTKEY =
       new FieldDef.Single<ChangeData, Long>(
           "sortkey2", FieldType.LONG, true) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
index 201c983..a710a10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -91,6 +91,7 @@
    * @param p the predicate to match. Must be a tree containing only AND, OR,
    *     or NOT predicates as internal nodes, and {@link IndexPredicate}s as
    *     leaves.
+   * @param start offset in results list at which to start returning results.
    * @param limit maximum number of results to return.
    * @return a source of documents matching the predicate. Documents must be
    *     returned in descending sort key order, unless a {@code sortkey_after}
@@ -101,8 +102,8 @@
    * @throws QueryParseException if the predicate could not be converted to an
    *     indexed data source.
    */
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
-      throws QueryParseException;
+  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
+      int limit) throws QueryParseException;
 
   /**
    * Mark whether this index is up-to-date and ready to serve reads.
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 475e7d1..a46b22d 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
@@ -38,7 +38,7 @@
         ChangeField.PROJECT,
         ChangeField.REF,
         ChangeField.TOPIC,
-        ChangeField.UPDATED,
+        ChangeField.LEGACY_UPDATED,
         ChangeField.LEGACY_SORTKEY,
         ChangeField.PATH,
         ChangeField.OWNER,
@@ -58,7 +58,7 @@
         ChangeField.PROJECT,
         ChangeField.REF,
         ChangeField.TOPIC,
-        ChangeField.UPDATED,
+        ChangeField.LEGACY_UPDATED,
         ChangeField.LEGACY_SORTKEY,
         ChangeField.PATH,
         ChangeField.OWNER,
@@ -72,6 +72,7 @@
         ChangeField.CHANGE,
         ChangeField.APPROVAL);
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V3 = release(
         ChangeField.LEGACY_ID,
         ChangeField.ID,
@@ -79,7 +80,7 @@
         ChangeField.PROJECT,
         ChangeField.REF,
         ChangeField.TOPIC,
-        ChangeField.UPDATED,
+        ChangeField.LEGACY_UPDATED,
         ChangeField.SORTKEY,
         ChangeField.PATH,
         ChangeField.OWNER,
@@ -96,6 +97,7 @@
   // For upgrade to Lucene 4.4.0 index format only.
   static final Schema<ChangeData> V4 = release(V3.getFields().values());
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V5 = release(
         ChangeField.LEGACY_ID,
         ChangeField.ID,
@@ -103,7 +105,7 @@
         ChangeField.PROJECT,
         ChangeField.REF,
         ChangeField.TOPIC,
-        ChangeField.UPDATED,
+        ChangeField.LEGACY_UPDATED,
         ChangeField.SORTKEY,
         ChangeField.PATH,
         ChangeField.OWNER,
@@ -121,6 +123,7 @@
   // For upgrade to Lucene 4.6.0 index format only.
   static final Schema<ChangeData> V6 = release(V5.getFields().values());
 
+  @SuppressWarnings("deprecation")
   static final Schema<ChangeData> V7 = release(
         ChangeField.LEGACY_ID,
         ChangeField.ID,
@@ -128,7 +131,7 @@
         ChangeField.PROJECT,
         ChangeField.REF,
         ChangeField.TOPIC,
-        ChangeField.UPDATED,
+        ChangeField.LEGACY_UPDATED,
         ChangeField.SORTKEY,
         ChangeField.FILE_PART,
         ChangeField.PATH,
@@ -144,6 +147,28 @@
         ChangeField.APPROVAL,
         ChangeField.MERGEABLE);
 
+  static final Schema<ChangeData> V8 = release(
+        ChangeField.LEGACY_ID,
+        ChangeField.ID,
+        ChangeField.STATUS,
+        ChangeField.PROJECT,
+        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);
+
 
   private static Schema<ChangeData> release(Collection<FieldDef<ChangeData, ?>> fields) {
     return new Schema<ChangeData>(true, fields);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
index 3e2ed0a..345f5b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.index;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Change;
@@ -130,13 +131,17 @@
   }
 
   @Override
-  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start)
       throws QueryParseException {
     ChangeIndex index = indexes.getSearchIndex();
     in = basicRewrites.rewrite(in);
-    int limit = ChangeQueryBuilder.hasLimit(in)
-        ? ChangeQueryBuilder.getLimit(in)
-        : MAX_LIMIT;
+    int limit =
+        Objects.firstNonNull(ChangeQueryBuilder.getLimit(in), MAX_LIMIT);
+    // Increase the limit rather than skipping, since we don't know how many
+    // skipped results would have been filtered out by the enclosing AndSource.
+    limit += start;
+    limit = Math.max(limit, 1);
+    limit = Math.min(limit, MAX_LIMIT);
 
     Predicate<ChangeData> out = rewriteImpl(in, index, limit);
     if (in == out || out instanceof IndexPredicate) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
index e506217..09d66e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
@@ -89,7 +89,7 @@
     this.index = index;
     this.limit = limit;
     this.pred = pred;
-    this.source = index.getSource(pred, limit);
+    this.source = index.getSource(pred, 0, limit);
   }
 
   @Override
@@ -166,7 +166,20 @@
   public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
     pred = replaceSortKeyPredicates(pred, last.change().getSortKey());
     try {
-      source = index.getSource(pred, limit);
+      source = index.getSource(pred, 0, limit);
+    } catch (QueryParseException e) {
+      // Don't need to show this exception to the user; the only thing that
+      // changed about pred was its SortKeyPredicates, and any other QPEs
+      // that might happen should have already thrown from the constructor.
+      throw new OrmException(e);
+    }
+    return read();
+  }
+
+  @Override
+  public ResultSet<ChangeData> restart(int start) throws OrmException {
+    try {
+      source = index.getSource(pred, start, limit);
     } catch (QueryParseException e) {
       // Don't need to show this exception to the user; the only thing that
       // changed about pred was its SortKeyPredicates, and any other QPEs
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
index 62fba12..0ba20e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/TimestampRangePredicate.java
@@ -14,14 +14,57 @@
 
 package com.google.gerrit.server.index;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.index.ChangeField.UPDATED;
+
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+
+import org.eclipse.jgit.util.GitDateParser;
+import org.joda.time.DateTime;
+
 import java.sql.Timestamp;
+import java.text.ParseException;
+import java.util.Date;
+import java.util.Locale;
 
 public abstract class TimestampRangePredicate<I> extends IndexPredicate<I> {
+  @SuppressWarnings({"deprecation", "unchecked"})
+  protected static FieldDef<ChangeData, Timestamp> updatedField(
+      Schema<ChangeData> schema) {
+    if (schema == null) {
+      return ChangeField.LEGACY_UPDATED;
+    }
+    FieldDef<ChangeData, ?> f = schema.getFields().get(UPDATED.getName());
+    if (f == null) {
+      f = schema.getFields().get(ChangeField.LEGACY_UPDATED.getName());
+      checkNotNull(f, "schema missing updated field, found: %s", schema);
+    }
+    checkArgument(f.getType() == FieldType.TIMESTAMP,
+        "expected %s to be TIMESTAMP, found %s", f.getName(), f.getType());
+    return (FieldDef<ChangeData, Timestamp>) f;
+  }
+
+  protected static Date parse(String value) throws QueryParseException {
+    try {
+      return GitDateParser.parse(value, DateTime.now().toCalendar(Locale.US));
+    } catch (ParseException e) {
+      // ParseException's message is specific and helpful, so preserve it.
+      throw new QueryParseException(e.getMessage(), e);
+    }
+  }
+
   protected TimestampRangePredicate(FieldDef<I, Timestamp> def,
       String name, String value) {
     super(def, name, value);
   }
 
-  public abstract Timestamp getMinTimestamp();
-  public abstract Timestamp getMaxTimestamp();
+  public abstract Date getMinTimestamp();
+  public abstract Date getMaxTimestamp();
+
+  @Override
+  public int getCost() {
+    return 1;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 102a136..c0cfe90 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -217,6 +217,9 @@
   }
 
   public ChangeControl forUser(final CurrentUser who) {
+    if (getCurrentUser().equals(who)) {
+      return this;
+    }
     return new ChangeControl(approvalsUtil, changeDataFactory,
         getRefControl().forUser(who), notes);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java
new file mode 100644
index 0000000..cb0038c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java
@@ -0,0 +1,47 @@
+// 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.server.query.change;
+
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.TimestampRangePredicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.Date;
+
+public class AfterPredicate extends TimestampRangePredicate<ChangeData> {
+  private final Date cut;
+
+  AfterPredicate(Schema<ChangeData> schema, String value)
+      throws QueryParseException {
+    super(updatedField(schema), ChangeQueryBuilder.FIELD_BEFORE, value);
+    cut = parse(value);
+  }
+
+  @Override
+  public Date getMinTimestamp() {
+    return cut;
+  }
+
+  @Override
+  public Date getMaxTimestamp() {
+    return new Date(Long.MAX_VALUE);
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    return cd.change().getLastUpdatedOn().getTime() >= cut.getTime();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
index 1913791..1894596 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -19,7 +19,7 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.index.TimestampRangePredicate;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.server.OrmException;
@@ -29,34 +29,27 @@
 public class AgePredicate extends TimestampRangePredicate<ChangeData> {
   private final long cut;
 
-  AgePredicate(String value) {
-    super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_AGE, value);
+  AgePredicate(Schema<ChangeData> schema, String value) {
+    super(updatedField(schema), ChangeQueryBuilder.FIELD_AGE, value);
 
     long s = ConfigUtil.getTimeUnit(getValue(), 0, SECONDS);
     long ms = MILLISECONDS.convert(s, SECONDS);
     this.cut = TimeUtil.nowMs() - ms;
   }
 
+  @Override
   public Timestamp getMinTimestamp() {
     return new Timestamp(0);
   }
 
+  @Override
   public Timestamp getMaxTimestamp() {
     return new Timestamp(cut);
   }
 
-  long getCut() {
-    return cut + 1;
-  }
-
   @Override
   public boolean match(final ChangeData object) throws OrmException {
     Change change = object.change();
     return change != null && change.getLastUpdatedOn().getTime() <= cut;
   }
-
-  @Override
-  public int getCost() {
-    return 1;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index a641edd..ad0ec3c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.Preconditions.checkArgument;
+
 import com.google.common.base.Function;
 import com.google.common.base.Throwables;
 import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.server.query.AndPredicate;
@@ -71,10 +74,18 @@
     return r;
   }
 
+  private final int start;
   private int cardinality = -1;
 
   public AndSource(Collection<? extends Predicate<ChangeData>> that) {
+    this(that, 0);
+  }
+
+  public AndSource(Collection<? extends Predicate<ChangeData>> that,
+      int start) {
     super(sort(that));
+    checkArgument(start >= 0, "negative start: %s", start);
+    this.start = start;
   }
 
   @Override
@@ -98,9 +109,13 @@
     if (source == null) {
       throw new OrmException("No ChangeDataSource: " + this);
     }
+    @SuppressWarnings("unchecked")
+    Predicate<ChangeData> pred = (Predicate<ChangeData>) source;
+    boolean useSortKey = ChangeQueryBuilder.hasSortKey(pred);
 
     List<ChangeData> r = Lists.newArrayList();
     ChangeData last = null;
+    int nextStart = 0;
     boolean skipped = false;
     for (ChangeData data : buffer(source, source.read())) {
       if (match(data)) {
@@ -109,6 +124,7 @@
         skipped = true;
       }
       last = data;
+      nextStart++;
     }
 
     if (skipped && last != null && source instanceof Paginated) {
@@ -117,21 +133,31 @@
       // limit the caller wants.  Restart the source and continue.
       //
       Paginated p = (Paginated) source;
-      while (skipped && r.size() < p.limit()) {
+      while (skipped && r.size() < p.limit() + start) {
         ChangeData lastBeforeRestart = last;
         skipped = false;
         last = null;
-        for (ChangeData data : buffer(source, p.restart(lastBeforeRestart))) {
+        ResultSet<ChangeData> next = useSortKey
+            ? p.restart(lastBeforeRestart)
+            : p.restart(nextStart);
+
+        for (ChangeData data : buffer(source, next)) {
           if (match(data)) {
             r.add(data);
           } else {
             skipped = true;
           }
           last = data;
+          nextStart++;
         }
       }
     }
 
+    if (start >= r.size()) {
+      r = ImmutableList.of();
+    } else if (start > 0) {
+      r = ImmutableList.copyOf(r.subList(start, r.size()));
+    }
     return new ListResultSet<ChangeData>(r);
   }
 
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 0ec4b5b..8ce6fa3 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
@@ -14,13 +14,8 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.IndexCollection;
-import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.query.IntPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryRewriter;
@@ -41,19 +36,12 @@
       new QueryRewriter.Definition<ChangeData, BasicChangeRewrites>(
           BasicChangeRewrites.class, BUILDER);
 
-  static Schema<ChangeData> schema(@Nullable IndexCollection indexes) {
-    ChangeIndex index = indexes != null ? indexes.getSearchIndex() : null;
-    return index != null ? index.getSchema() : null;
-  }
-
   protected final Provider<ReviewDb> dbProvider;
-  private final IndexCollection indexes;
 
   @Inject
-  public BasicChangeRewrites(Provider<ReviewDb> dbProvider, IndexCollection indexes) {
+  public BasicChangeRewrites(Provider<ReviewDb> dbProvider) {
     super(mydef);
     this.dbProvider = dbProvider;
-    this.indexes = indexes;
   }
 
   @Rewrite("-status:open")
@@ -84,14 +72,6 @@
         new ChangeStatusPredicate(Change.Status.MERGED));
   }
 
-  @SuppressWarnings("unchecked")
-  @NoCostComputation
-  @Rewrite("sortkey_before:z A=(age:*)")
-  public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) {
-    String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE);
-    return and(new SortKeyPredicate.Before(schema(indexes), dbProvider, cut), a);
-  }
-
   @NoCostComputation
   @Rewrite("A=(limit:*) B=(limit:*)")
   public Predicate<ChangeData> r00_smallestLimit(
@@ -100,21 +80,6 @@
     return a.intValue() <= b.intValue() ? a : b;
   }
 
-  @NoCostComputation
-  @Rewrite("A=(sortkey_before:*) B=(sortkey_before:*)")
-  public Predicate<ChangeData> r00_oldestSortKey(
-      @Named("A") SortKeyPredicate.Before a,
-      @Named("B") SortKeyPredicate.Before b) {
-    return a.getValue().compareTo(b.getValue()) <= 0 ? a : b;
-  }
-
-  @NoCostComputation
-  @Rewrite("A=(sortkey_after:*) B=(sortkey_after:*)")
-  public Predicate<ChangeData> r00_newestSortKey(
-      @Named("A") SortKeyPredicate.After a, @Named("B") SortKeyPredicate.After b) {
-    return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
-  }
-
   private static final class InvalidProvider<T> implements Provider<T> {
     @Override
     public T get() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java
new file mode 100644
index 0000000..f724676
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java
@@ -0,0 +1,47 @@
+// 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.server.query.change;
+
+import com.google.gerrit.server.index.Schema;
+import com.google.gerrit.server.index.TimestampRangePredicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gwtorm.server.OrmException;
+
+import java.util.Date;
+
+public class BeforePredicate extends TimestampRangePredicate<ChangeData> {
+  private final Date cut;
+
+  BeforePredicate(Schema<ChangeData> schema, String value)
+      throws QueryParseException {
+    super(updatedField(schema), ChangeQueryBuilder.FIELD_BEFORE, value);
+    cut = parse(value);
+  }
+
+  @Override
+  public Date getMinTimestamp() {
+    return new Date(0);
+  }
+
+  @Override
+  public Date getMaxTimestamp() {
+    return cut;
+  }
+
+  @Override
+  public boolean match(ChangeData cd) throws OrmException {
+    return cd.change().getLastUpdatedOn().getTime() <= cut.getTime();
+  }
+}
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 968b2c04..333c343 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
@@ -33,6 +33,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
@@ -150,11 +151,13 @@
    * @return instance for testing.
    */
   static ChangeData createForTest(Change.Id id) {
-    return new ChangeData(null, null, null, null, null, null, id);
+    return new ChangeData(null, null, null, null, null, null, null, null, id);
   }
 
   private final ReviewDb db;
   private final GitRepositoryManager repoManager;
+  private final ChangeControl.GenericFactory changeControlFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeNotes.Factory notesFactory;
   private final ApprovalsUtil approvalsUtil;
   private final PatchListCache patchListCache;
@@ -180,6 +183,8 @@
   @AssistedInject
   private ChangeData(
       GitRepositoryManager repoManager,
+      ChangeControl.GenericFactory changeControlFactory,
+      IdentifiedUser.GenericFactory userFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       PatchListCache patchListCache,
@@ -188,6 +193,8 @@
       @Assisted Change.Id id) {
     this.db = db;
     this.repoManager = repoManager;
+    this.changeControlFactory = changeControlFactory;
+    this.userFactory = userFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.patchListCache = patchListCache;
@@ -198,6 +205,8 @@
   @AssistedInject
   private ChangeData(
       GitRepositoryManager repoManager,
+      ChangeControl.GenericFactory changeControlFactory,
+      IdentifiedUser.GenericFactory userFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       PatchListCache patchListCache,
@@ -206,6 +215,8 @@
       @Assisted Change c) {
     this.db = db;
     this.repoManager = repoManager;
+    this.changeControlFactory = changeControlFactory;
+    this.userFactory = userFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.patchListCache = patchListCache;
@@ -217,6 +228,8 @@
   @AssistedInject
   private ChangeData(
       GitRepositoryManager repoManager,
+      ChangeControl.GenericFactory changeControlFactory,
+      IdentifiedUser.GenericFactory userFactory,
       ChangeNotes.Factory notesFactory,
       ApprovalsUtil approvalsUtil,
       PatchListCache patchListCache,
@@ -225,6 +238,8 @@
       @Assisted ChangeControl c) {
     this.db = db;
     this.repoManager = repoManager;
+    this.changeControlFactory = changeControlFactory;
+    this.userFactory = userFactory;
     this.notesFactory = notesFactory;
     this.approvalsUtil = approvalsUtil;
     this.patchListCache = patchListCache;
@@ -324,7 +339,17 @@
     return visibleTo == user;
   }
 
-  public ChangeControl changeControl() {
+  public boolean hasChangeControl() {
+    return changeControl != null;
+  }
+
+  public ChangeControl changeControl() throws NoSuchChangeException,
+      OrmException {
+    if (changeControl == null) {
+      Change c = change();
+      changeControl =
+          changeControlFactory.controlFor(c, userFactory.create(c.getOwner()));
+    }
     return changeControl;
   }
 
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 01c0e4a..bf92867 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
@@ -16,6 +16,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -84,7 +85,9 @@
   // NOTE: As new search operations are added, please keep the
   // SearchSuggestOracle up to date.
 
+  public static final String FIELD_AFTER = "after";
   public static final String FIELD_AGE = "age";
+  public static final String FIELD_BEFORE = "before";
   public static final String FIELD_BRANCH = "branch";
   public static final String FIELD_CHANGE = "change";
   public static final String FIELD_COMMENT = "comment";
@@ -122,13 +125,10 @@
           ChangeQueryBuilder.class);
 
   @SuppressWarnings("unchecked")
-  public static boolean hasLimit(Predicate<ChangeData> p) {
-    return find(p, IntPredicate.class, FIELD_LIMIT) != null;
-  }
-
-  @SuppressWarnings("unchecked")
-  public static int getLimit(Predicate<ChangeData> p) {
-    return ((IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT)).intValue();
+  public static Integer getLimit(Predicate<ChangeData> p) {
+    IntPredicate<?> ip =
+        (IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT);
+    return ip != null ? ip.intValue() : null;
   }
 
   public static boolean hasNonTrivialSortKeyAfter(Schema<ChangeData> schema,
@@ -235,7 +235,27 @@
 
   @Operator
   public Predicate<ChangeData> age(String value) {
-    return new AgePredicate(value);
+    return new AgePredicate(schema(args.indexes), value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> before(String value) throws QueryParseException {
+    return new BeforePredicate(schema(args.indexes), value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> until(String value) throws QueryParseException {
+    return before(value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> after(String value) throws QueryParseException {
+    return new AfterPredicate(schema(args.indexes), value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> since(String value) throws QueryParseException {
+    return after(value);
   }
 
   @Operator
@@ -638,16 +658,18 @@
     return new LimitPredicate(limit);
   }
 
+  boolean supportsSortKey() {
+    return SortKeyPredicate.hasSortKeyField(schema(args.indexes));
+  }
+
   @Operator
   public Predicate<ChangeData> sortkey_after(String sortKey) {
-    return new SortKeyPredicate.After(
-        BasicChangeRewrites.schema(args.indexes), args.db, sortKey);
+    return new SortKeyPredicate.After(schema(args.indexes), args.db, sortKey);
   }
 
   @Operator
   public Predicate<ChangeData> sortkey_before(String sortKey) {
-    return new SortKeyPredicate.Before(
-        BasicChangeRewrites.schema(args.indexes), args.db, sortKey);
+    return new SortKeyPredicate.Before(schema(args.indexes), args.db, sortKey);
   }
 
   @Operator
@@ -756,4 +778,9 @@
     }
     throw new IllegalArgumentException();
   }
+
+  private static Schema<ChangeData> schema(@Nullable IndexCollection indexes) {
+    ChangeIndex index = indexes != null ? indexes.getSearchIndex() : null;
+    return index != null ? index.getSchema() : null;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index bd186c7..bbf235b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -18,6 +18,6 @@
 import com.google.gerrit.server.query.QueryParseException;
 
 public interface ChangeQueryRewriter {
-  Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+  Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start)
       throws QueryParseException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
index 6cfb897..5994e5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -36,7 +36,7 @@
   public boolean match(ChangeData object) throws OrmException {
     try {
       for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(args, object.getId()), this), 1)
+          Predicate.and(new LegacyChangeIdPredicate(args, object.getId()), this), 0, 1)
           .read()) {
         if (cData.getId().equals(object.getId())) {
           return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index f81cf51..04bdb1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -40,7 +40,7 @@
   public boolean match(ChangeData object) throws OrmException {
     try {
       for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(args, object.getId()), this), 1)
+          Predicate.and(new LegacyChangeIdPredicate(args, object.getId()), this), 0, 1)
           .read()) {
         if (cData.getId().equals(object.getId())) {
           return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
index d9ff80c..e411cf9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
@@ -21,4 +21,6 @@
   int limit();
 
   ResultSet<ChangeData> restart(ChangeData last) throws OrmException;
+
+  ResultSet<ChangeData> restart(int start) throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index 081f135..41c2e23 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -80,6 +80,11 @@
     imp.setSortkeyBefore(key);
   }
 
+  @Option(name = "-S", metaVar = "CNT", usage = "Number of changes to skip")
+  public void setStart(int start) {
+    imp.setStart(start);
+  }
+
   @Inject
   QueryChanges(ChangeJson json, QueryProcessor qp, Provider<CurrentUser> user) {
     this.json = json;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 81767ae..33208863 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -97,13 +98,13 @@
   private final ChangeQueryBuilder queryBuilder;
   private final ChangeQueryRewriter queryRewriter;
   private final Provider<ReviewDb> db;
-  private final ChangeControl.GenericFactory changeControlFactory;
   private final TrackingFooters trackingFooters;
   private final CurrentUser user;
   private final int maxLimit;
 
   private OutputFormat outputFormat = OutputFormat.TEXT;
   private int limit;
+  private int start;
   private String sortkeyAfter;
   private String sortkeyBefore;
   private boolean includePatchSets;
@@ -124,14 +125,12 @@
   QueryProcessor(EventFactory eventFactory,
       ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
       ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db,
-      TrackingFooters trackingFooters,
-      ChangeControl.GenericFactory changeControlFactory) {
+      TrackingFooters trackingFooters) {
     this.eventFactory = eventFactory;
     this.queryBuilder = queryBuilder.create(currentUser);
     this.queryRewriter = queryRewriter;
     this.db = db;
     this.trackingFooters = trackingFooters;
-    this.changeControlFactory = changeControlFactory;
     this.user = currentUser;
     this.maxLimit = currentUser.getCapabilities()
       .getRange(GlobalCapability.QUERY_LIMIT)
@@ -147,6 +146,10 @@
     limit = n;
   }
 
+  void setStart(int n) {
+    start = n;
+  }
+
   void setSortkeyAfter(String sortkey) {
     sortkeyAfter = sortkey;
   }
@@ -243,17 +246,17 @@
     List<ChangeDataSource> sources = Lists.newArrayListWithCapacity(cnt);
     for (String query : queries) {
       Predicate<ChangeData> q = parseQuery(query, visibleToMe);
-      Predicate<ChangeData> s = queryRewriter.rewrite(q);
+      Predicate<ChangeData> s = queryRewriter.rewrite(q, start);
       if (!(s instanceof ChangeDataSource)) {
         q = Predicate.and(queryBuilder.status_open(), q);
-        s = queryRewriter.rewrite(q);
+        s = queryRewriter.rewrite(q, start);
       }
       if (!(s instanceof ChangeDataSource)) {
         throw new QueryParseException("invalid query: " + s);
       }
 
       // Don't trust QueryRewriter to have left the visible predicate.
-      AndSource a = new AndSource(ImmutableList.of(s, visibleToMe));
+      AndSource a = new AndSource(ImmutableList.of(s, visibleToMe), start);
       limits.add(limit(q));
       sources.add(a);
     }
@@ -267,7 +270,11 @@
     List<List<ChangeData>> out = Lists.newArrayListWithCapacity(cnt);
     for (int i = 0; i < cnt; i++) {
       List<ChangeData> results = matches.get(i).toList();
-      Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
+      if (sortkeyAfter != null) {
+        Collections.sort(results, cmpAfter);
+      } else if (sortkeyBefore != null) {
+        Collections.sort(results, cmpBefore);
+      }
       if (results.size() > maxLimit) {
         moreResults = true;
       }
@@ -302,10 +309,7 @@
         List<ChangeData> results = queryChanges(queryString);
         ChangeAttribute c = null;
         for (ChangeData d : results) {
-          ChangeControl cc = d.changeControl();
-          if (cc == null || cc.getCurrentUser() != user) {
-            cc = changeControlFactory.controlFor(d.change(), user);
-          }
+          ChangeControl cc = d.changeControl().forUser(user);
 
           LabelTypes labelTypes = cc.getLabelTypes();
           c = eventFactory.asChangeAttribute(d.change());
@@ -412,16 +416,14 @@
   }
 
   private int limit(Predicate<ChangeData> s) {
-    int n = ChangeQueryBuilder.hasLimit(s)
-        ? ChangeQueryBuilder.getLimit(s)
-        : maxLimit;
+    int n = Objects.firstNonNull(ChangeQueryBuilder.getLimit(s), maxLimit);
     return limit > 0 ? Math.min(n, limit) + 1 : n + 1;
   }
 
   private Predicate<ChangeData> parseQuery(String queryString,
       final Predicate<ChangeData> visibleToMe) throws QueryParseException {
     Predicate<ChangeData> q = queryBuilder.parse(queryString);
-    if (!ChangeQueryBuilder.hasSortKey(q)) {
+    if (queryBuilder.supportsSortKey() && !ChangeQueryBuilder.hasSortKey(q)) {
       if (sortkeyBefore != null) {
         q = Predicate.and(q, queryBuilder.sortkey_before(sortkeyBefore));
       } else if (sortkeyAfter != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
index 720027b..6fa11fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
@@ -29,6 +29,10 @@
 import com.google.inject.Provider;
 
 public abstract class SortKeyPredicate extends IndexPredicate<ChangeData> {
+  public static boolean hasSortKeyField(Schema<ChangeData> schema) {
+    return sortkeyFieldOrNull(schema) != null;
+  }
+
   @SuppressWarnings("deprecation")
   private static long parseSortKey(Schema<ChangeData> schema, String value) {
     FieldDef<ChangeData, ?> field = schema.getFields().get(SORTKEY.getName());
@@ -40,7 +44,8 @@
   }
 
   @SuppressWarnings("deprecation")
-  private static FieldDef<ChangeData, ?> sortkeyField(Schema<ChangeData> schema) {
+  private static FieldDef<ChangeData, ?> sortkeyFieldOrNull(
+      Schema<ChangeData> schema) {
     if (schema == null) {
       return ChangeField.LEGACY_SORTKEY;
     }
@@ -48,9 +53,13 @@
     if (f != null) {
       return f;
     }
+    return schema.getFields().get(ChangeField.LEGACY_SORTKEY.getName());
+  }
+
+  private static FieldDef<ChangeData, ?> sortkeyField(Schema<ChangeData> schema) {
     return checkNotNull(
-        schema.getFields().get(ChangeField.LEGACY_SORTKEY.getName()),
-        "schema missing sortkey field, found: %s", schema.getFields().keySet());
+        sortkeyFieldOrNull(schema),
+        "schema missing sortkey field, found: %s", schema);
   }
 
   protected final Schema<ChangeData> schema;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
index 624a51b..636e0c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
@@ -17,7 +17,6 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.common.primitives.Longs;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -448,7 +447,7 @@
           @Override
           public int compare(
               AccountGroupAgreement a1, AccountGroupAgreement a2) {
-            return Longs.compare(a1.getTime(), a2.getTime());
+            return Long.compare(a1.getTime(), a2.getTime());
           }
         });
         return groupAgreements;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
index eb92bf9..33ba36e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
@@ -30,8 +30,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.util.TimeUtil;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.ListResultSet;
@@ -81,7 +79,6 @@
   private Provider<String> urlProvider;
   private GitRepositoryManager repoManager;
   private GitReferenceUpdated gitRefUpdated;
-  private ChangeNotes.Factory notesFactory;
 
   @SuppressWarnings("unchecked")
   @Override
@@ -95,7 +92,6 @@
     urlProvider = createStrictMock(Provider.class);
     repoManager = createStrictMock(GitRepositoryManager.class);
     gitRefUpdated = createStrictMock(GitReferenceUpdated.class);
-    notesFactory = new ChangeNotes.Factory(repoManager);
   }
 
   private void doReplay() {
@@ -616,11 +612,10 @@
     final Change submittedChange = new Change(
         new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
         new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
-    codeReviewCommit.notes = notesFactory.create(submittedChange);
 
     final Map<Change.Id, CodeReviewCommit> mergedCommits =
         new HashMap<Change.Id, CodeReviewCommit>();
-    mergedCommits.put(codeReviewCommit.notes.getChangeId(), codeReviewCommit);
+    mergedCommits.put(submittedChange.getId(), codeReviewCommit);
 
     final List<Change> submitted = new ArrayList<Change>();
     submitted.add(submittedChange);
@@ -720,11 +715,10 @@
     final Change submittedChange = new Change(
         new Change.Key(sourceMergeTip.toObjectId().getName()), new Change.Id(1),
         new Account.Id(1), sourceBranchNameKey, TimeUtil.nowTs());
-    codeReviewCommit.notes = notesFactory.create(submittedChange);
 
     final Map<Change.Id, CodeReviewCommit> mergedCommits =
         new HashMap<Change.Id, CodeReviewCommit>();
-    mergedCommits.put(codeReviewCommit.notes.getChangeId(), codeReviewCommit);
+    mergedCommits.put(submittedChange.getId(), codeReviewCommit);
 
     final List<Change> submitted = new ArrayList<Change>();
     submitted.add(submittedChange);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
index 339a1bb..4db3b27 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
@@ -31,7 +31,7 @@
     ImmutableList.of(
       ChangeField.STATUS,
       ChangeField.PATH,
-      ChangeField.SORTKEY));
+      ChangeField.UPDATED));
 
   private static class Source implements ChangeDataSource {
     private final Predicate<ChangeData> p;
@@ -88,8 +88,8 @@
   }
 
   @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
-      throws QueryParseException {
+  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
+      int limit) throws QueryParseException {
     return new FakeIndex.Source(p);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
index 87683e1..c8275e8 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
@@ -37,6 +37,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Set;
 
@@ -54,7 +55,7 @@
     queryBuilder = new FakeQueryBuilder(indexes);
     rewrite = new IndexRewriteImpl(
         indexes,
-        new BasicChangeRewrites(null, indexes));
+        new BasicChangeRewrites(null));
   }
 
   @Test
@@ -97,7 +98,7 @@
         parse("-status:abandoned (status:open OR status:merged)");
     assertEquals(
         query(parse("status:new OR status:submitted OR status:draft OR status:merged")),
-        rewrite.rewrite(in));
+        rewrite.rewrite(in, 0));
   }
 
   @Test
@@ -169,6 +170,23 @@
   }
 
   @Test
+  public void testStartIncreasesLimit() throws Exception {
+    Predicate<ChangeData> f = parse("file:a");
+    Predicate<ChangeData> l = parse("limit:3");
+    Predicate<ChangeData> in = and(f, l);
+    assertEquals(and(query(f, 3), l), rewrite.rewrite(in, 0));
+    assertEquals(and(query(f, 4), l), rewrite.rewrite(in, 1));
+    assertEquals(and(query(f, 5), l), rewrite.rewrite(in, 2));
+  }
+
+  @Test
+  public void testStartDoesNotExceedMaxLimit() throws Exception {
+    Predicate<ChangeData> in = parse("file:a");
+    assertEquals(query(in), rewrite.rewrite(in, 0));
+    assertEquals(query(in), rewrite.rewrite(in, 1));
+  }
+
+  @Test
   public void testGetPossibleStatus() throws Exception {
     assertEquals(EnumSet.allOf(Change.Status.class), status("file:a"));
     assertEquals(EnumSet.of(NEW), status("is:new"));
@@ -203,9 +221,14 @@
     return queryBuilder.parse(query);
   }
 
+  @SafeVarargs
+  private static AndSource and(Predicate<ChangeData>... preds) {
+    return new AndSource(Arrays.asList(preds));
+  }
+
   private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
       throws QueryParseException {
-    return rewrite.rewrite(in);
+    return rewrite.rewrite(in, 0);
   }
 
   private IndexedChangeQuery query(Predicate<ChangeData> p)
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexedChangeQueryTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexedChangeQueryTest.java
deleted file mode 100644
index 3d21902..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexedChangeQueryTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.index;
-
-import static com.google.gerrit.server.index.IndexedChangeQuery.replaceSortKeyPredicates;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertSame;
-
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import org.junit.Before;
-import org.junit.Test;
-
-public class IndexedChangeQueryTest {
-  private FakeIndex index;
-  private ChangeQueryBuilder queryBuilder;
-
-  @Before
-  public void setUp() throws Exception {
-    index = new FakeIndex(FakeIndex.V2);
-    IndexCollection indexes = new IndexCollection();
-    indexes.setSearchIndex(index);
-    queryBuilder = new FakeQueryBuilder(indexes);
-  }
-
-  @Test
-  public void testReplaceSortKeyPredicate_NoSortKey() throws Exception {
-    Predicate<ChangeData> p = parse("foo:a bar:b OR (foo:b bar:a)");
-    assertSame(p, replaceSortKeyPredicates(p, "1234"));
-  }
-
-  @Test
-  public void testReplaceSortKeyPredicate_TopLevelSortKey() throws Exception {
-    Predicate<ChangeData> p;
-    p = parse("foo:a bar:b sortkey_before:1234 OR (foo:b bar:a)");
-    assertEquals(parse("foo:a bar:b sortkey_before:5678 OR (foo:b bar:a)"),
-        replaceSortKeyPredicates(p, "5678"));
-    p = parse("foo:a bar:b sortkey_after:1234 OR (foo:b bar:a)");
-    assertEquals(parse("foo:a bar:b sortkey_after:5678 OR (foo:b bar:a)"),
-        replaceSortKeyPredicates(p, "5678"));
-  }
-
-  @Test
-  public void testReplaceSortKeyPredicate_NestedSortKey() throws Exception {
-    Predicate<ChangeData> p;
-    p = parse("foo:a bar:b OR (foo:b bar:a AND sortkey_before:1234)");
-    assertEquals(parse("foo:a bar:b OR (foo:b bar:a sortkey_before:5678)"),
-        replaceSortKeyPredicates(p, "5678"));
-    p = parse("foo:a bar:b OR (foo:b bar:a AND sortkey_after:1234)");
-    assertEquals(parse("foo:a bar:b OR (foo:b bar:a sortkey_after:5678)"),
-        replaceSortKeyPredicates(p, "5678"));
-  }
-
-  private Predicate<ChangeData> parse(String query) throws QueryParseException {
-    return queryBuilder.parse(query);
-  }
-}
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 b3def07..0d7bb9f 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
@@ -19,9 +19,7 @@
 import static com.google.gerrit.server.project.Util.category;
 import static com.google.gerrit.server.project.Util.value;
 import static com.google.inject.Scopes.SINGLETON;
-import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.easymock.EasyMock.expect;
 import static org.junit.Assert.assertEquals;
@@ -38,7 +36,6 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
@@ -73,6 +70,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.joda.time.DateTime;
 import org.joda.time.DateTimeUtils;
 import org.joda.time.DateTimeUtils.MillisProvider;
 import org.junit.After;
@@ -107,11 +105,12 @@
   private IdentifiedUser changeOwner;
   private IdentifiedUser otherUser;
   private Injector injector;
+  private String systemTimeZone;
   private volatile long clockStepMs;
 
   @Before
   public void setUp() throws Exception {
-    setMillisProvider();
+    setTimeForTesting();
 
     serverIdent = new PersonIdent(
         "Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ);
@@ -159,11 +158,11 @@
     otherUser = userFactory.create(ou.getId());
   }
 
-  private void setMillisProvider() {
+  private void setTimeForTesting() {
+    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
     clockStepMs = MILLISECONDS.convert(1, SECONDS);
     final AtomicLong clockMs = new AtomicLong(
-        MILLISECONDS.convert(ChangeUtil.SORT_KEY_EPOCH_MINS, MINUTES)
-        + MILLISECONDS.convert(60, DAYS));
+        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
 
     DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
       @Override
@@ -174,8 +173,9 @@
   }
 
   @After
-  public void resetMillisProvider() {
+  public void resetTime() {
     DateTimeUtils.setCurrentMillisSystem();
+    System.setProperty("user.timezone", systemTimeZone);
   }
 
   @Test
@@ -207,7 +207,7 @@
       assertEquals("1@gerrit", author.getEmailAddress());
       assertEquals(new Date(c.getCreatedOn().getTime() + 1000),
           author.getWhen());
-      assertEquals(TimeZone.getTimeZone("GMT-8:00"), author.getTimeZone());
+      assertEquals(TimeZone.getTimeZone("GMT-7:00"), author.getTimeZone());
 
       PersonIdent committer = commit.getCommitterIdent();
       assertEquals("Gerrit Server", committer.getName());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
index d248c54..e4de9ed 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
@@ -29,20 +29,29 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.testutil.FakeAccountCache;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -193,6 +202,7 @@
       protected void configure() {
         bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(
             new Config());
+        bind(ReviewDb.class).toProvider(Providers.<ReviewDb> of(null));
         bind(GitRepositoryManager.class).toInstance(repoManager);
         bind(PatchListCache.class)
             .toProvider(Providers.<PatchListCache> of(null));
@@ -201,6 +211,12 @@
         factory(ChangeControl.AssistedFactory.class);
         factory(ChangeData.Factory.class);
         bind(ProjectCache.class).toInstance(projectCache);
+        bind(AccountCache.class).toInstance(new FakeAccountCache());
+        bind(GroupBackend.class).to(SystemGroupBackend.class);
+        bind(String.class).annotatedWith(CanonicalWebUrl.class)
+            .toProvider(CanonicalWebUrlProvider.class);
+        bind(String.class).annotatedWith(AnonymousCowardName.class)
+            .toProvider(AnonymousCowardNameProvider.class);
       }
     });
 
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 93300e3..295f841 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
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static org.junit.Assert.assertEquals;
@@ -35,7 +35,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountManager;
@@ -61,6 +60,7 @@
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.joda.time.DateTime;
 import org.joda.time.DateTimeUtils;
 import org.joda.time.DateTimeUtils.MillisProvider;
 import org.junit.After;
@@ -94,6 +94,8 @@
   protected CurrentUser user;
   protected volatile long clockStepMs;
 
+  private String systemTimeZone;
+
   protected abstract Injector createInjector();
 
   @Before
@@ -136,11 +138,11 @@
   }
 
   @Before
-  public void setMillisProvider() {
+  public void setTimeForTesting() {
+    systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
     clockStepMs = 1;
     final AtomicLong clockMs = new AtomicLong(
-        MILLISECONDS.convert(ChangeUtil.SORT_KEY_EPOCH_MINS, MINUTES)
-        + MILLISECONDS.convert(60, DAYS));
+        new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
 
     DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
       @Override
@@ -151,8 +153,9 @@
   }
 
   @After
-  public void resetMillisProvider() {
+  public void resetTime() {
     DateTimeUtils.setCurrentMillisSystem();
+    System.setProperty("user.timezone", systemTimeZone);
   }
 
   @Test
@@ -429,51 +432,104 @@
   }
 
   @Test
-  public void pagination() throws Exception {
+  public void start() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
     List<Change> changes = Lists.newArrayList();
-    for (int i = 0; i < 5; i++) {
+    for (int i = 0; i < 2; i++) {
       changes.add(newChange(repo, null, null, null, null).insert());
     }
 
-    // Page forward and back through 3 pages of results.
+    QueryChanges q;
+    List<ChangeInfo> results;
+    results = query("status:new");
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(1), results.get(0));
+    assertResultEquals(changes.get(0), results.get(1));
+
+    q = newQuery("status:new");
+    q.setStart(1);
+    results = query(q);
+    assertEquals(1, results.size());
+    assertResultEquals(changes.get(0), results.get(0));
+
+    q = newQuery("status:new");
+    q.setStart(2);
+    results = query(q);
+    assertEquals(0, results.size());
+
+    q = newQuery("status:new");
+    q.setStart(3);
+    results = query(q);
+    assertEquals(0, results.size());
+  }
+
+  @Test
+  public void startWithLimit() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    List<Change> changes = Lists.newArrayList();
+    for (int i = 0; i < 3; i++) {
+      changes.add(newChange(repo, null, null, null, null).insert());
+    }
+
     QueryChanges q;
     List<ChangeInfo> results;
     results = query("status:new limit:2");
     assertEquals(2, results.size());
-    assertResultEquals(changes.get(4), results.get(0));
-    assertResultEquals(changes.get(3), results.get(1));
-
-    q = newQuery("status:new limit:2");
-    q.setSortKeyBefore(results.get(1)._sortkey);
-    results = query(q);
-    assertEquals(2, results.size());
     assertResultEquals(changes.get(2), results.get(0));
     assertResultEquals(changes.get(1), results.get(1));
 
     q = newQuery("status:new limit:2");
-    q.setSortKeyBefore(results.get(1)._sortkey);
+    q.setStart(1);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(1), results.get(0));
+    assertResultEquals(changes.get(0), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setStart(2);
     results = query(q);
     assertEquals(1, results.size());
     assertResultEquals(changes.get(0), results.get(0));
 
     q = newQuery("status:new limit:2");
-    q.setSortKeyAfter(results.get(0)._sortkey);
+    q.setStart(3);
     results = query(q);
-    assertEquals(2, results.size());
-    assertResultEquals(changes.get(2), results.get(0));
-    assertResultEquals(changes.get(1), results.get(1));
-
-    q = newQuery("status:new limit:2");
-    q.setSortKeyAfter(results.get(0)._sortkey);
-    results = query(q);
-    assertEquals(2, results.size());
-    assertResultEquals(changes.get(4), results.get(0));
-    assertResultEquals(changes.get(3), results.get(1));
+    assertEquals(0, results.size());
   }
 
   @Test
-  public void sortKeyWithMinuteResolution() throws Exception {
+  public void updateOrder() throws Exception {
+    clockStepMs = MILLISECONDS.convert(2, MINUTES);
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    List<ChangeInserter> inserters = Lists.newArrayList();
+    List<Change> changes = Lists.newArrayList();
+    for (int i = 0; i < 5; i++) {
+      inserters.add(newChange(repo, null, null, null, null));
+      changes.add(inserters.get(i).insert());
+    }
+
+    for (int i : ImmutableList.of(2, 0, 1, 4, 3)) {
+      ReviewInput input = new ReviewInput();
+      input.message = "modifying " + i;
+      postReview.apply(
+          new RevisionResource(
+            this.changes.parse(changes.get(i).getId()),
+            inserters.get(i).getPatchSet()),
+          input);
+      changes.set(i, db.changes().get(changes.get(i).getId()));
+    }
+
+    List<ChangeInfo> results = query("status:new");
+    assertEquals(5, results.size());
+    assertResultEquals(changes.get(3), results.get(0));
+    assertResultEquals(changes.get(4), results.get(1));
+    assertResultEquals(changes.get(1), results.get(2));
+    assertResultEquals(changes.get(0), results.get(3));
+    assertResultEquals(changes.get(2), results.get(4));
+  }
+
+  @Test
+  public void updatedOrderWithMinuteResolution() throws Exception {
     clockStepMs = MILLISECONDS.convert(2, MINUTES);
     TestRepository<InMemoryRepository> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
@@ -506,7 +562,7 @@
   }
 
   @Test
-  public void sortKeyWithSubMinuteResolution() throws Exception {
+  public void updatedOrderWithSubMinuteResolution() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
     Change change1 = ins1.insert();
@@ -532,23 +588,9 @@
 
     results = query("status:new");
     assertEquals(2, results.size());
-    // Same order as before change1 was modified.
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
-  }
-
-  @Test
-  public void sortKeyBreaksTiesOnChangeId() throws Exception {
-    clockStepMs = 0;
-    TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
-    Change change2 = newChange(repo, null, null, null, null).insert();
-    assertEquals(change1.getLastUpdatedOn(), change2.getLastUpdatedOn());
-
-    List<ChangeInfo> results = query("status:new");
-    assertEquals(2, results.size());
-    assertResultEquals(change2, results.get(0));
-    assertResultEquals(change1, results.get(1));
+    // change1 moved to the top.
+    assertResultEquals(change1, results.get(0));
+    assertResultEquals(change2, results.get(1));
   }
 
   @Test
@@ -561,7 +603,7 @@
       newChange(repo, null, null, user2, null).insert();
     }
 
-    assertResultEquals(change, queryOne("status:new ownerin:Administrators"));
+    //assertResultEquals(change, queryOne("status:new ownerin:Administrators"));
     assertResultEquals(change,
         queryOne("status:new ownerin:Administrators limit:2"));
   }
@@ -661,6 +703,130 @@
     assertResultEquals(change, queryOne("comment:inline"));
   }
 
+  @Test
+  public void byAge() throws Exception {
+    long thirtyHours = MILLISECONDS.convert(30, HOURS);
+    clockStepMs = thirtyHours;
+    TestRepository<InMemoryRepository> 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.
+    long now = TimeUtil.nowMs();
+    assertEquals(thirtyHours, lastUpdatedMs(change2) - lastUpdatedMs(change1));
+    assertEquals(thirtyHours, now - lastUpdatedMs(change2));
+    assertEquals(now, TimeUtil.nowMs());
+
+    assertTrue(query("-age:1d").isEmpty());
+    assertTrue(query("-age:" + (30*60-1) + "m").isEmpty());
+    assertResultEquals(change2, queryOne("-age:2d"));
+
+    List<ChangeInfo> results;
+    results = query("-age:3d");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+
+    assertTrue(query("age:3d").isEmpty());
+    assertResultEquals(change1, queryOne("age:2d"));
+
+    results = query("age:1d");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void byBeforeAbsolute() throws Exception {
+    clockStepMs = MILLISECONDS.convert(30, HOURS);
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+    clockStepMs = 0;
+
+    // GitDateParser drops unparsed portions of the string, so be very careful
+    // with formats.
+    assertTrue(query("before:2009-09-29").isEmpty());
+    assertTrue(query("before:2009-09-30").isEmpty());
+    assertTrue(query("before:\"2009-09-30 16:59:00 -0400\"").isEmpty());
+    assertResultEquals(change1,
+        queryOne("before:\"2009-09-30 21:02:00 -0400\""));
+    assertResultEquals(change1, queryOne("before:2009-10-01"));
+
+    List<ChangeInfo> results;
+    results = query("before:2009-10-03");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void byBeforeRelative() throws Exception {
+    clockStepMs = MILLISECONDS.convert(30, HOURS);
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+    clockStepMs = 0;
+
+    assertTrue(query("before:\"3 days ago\"").isEmpty());
+    assertResultEquals(change1, queryOne("before:\"2 days ago\""));
+
+    List<ChangeInfo> results;
+    results = query("before:\"1 day ago\"");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+
+    results = query("before:\"12 hours ago\"");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void byAfterAbsolute() throws Exception {
+    clockStepMs = MILLISECONDS.convert(30, HOURS);
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+    clockStepMs = 0;
+
+    // GitDateParser drops unparsed portions of the string, so be very careful
+    // with formats.
+    assertTrue(query("after:2009-10-02").isEmpty());
+    assertResultEquals(change2,
+        queryOne("after:\"2009-10-01 20:59:59 -0400\""));
+    assertResultEquals(change2, queryOne("after:2009-10-01"));
+
+    List<ChangeInfo> results;
+    results = query("after:2009-09-30");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void byAfterRelative() throws Exception {
+    clockStepMs = MILLISECONDS.convert(30, HOURS);
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+    clockStepMs = 0;
+
+    assertTrue(query("after:\"1 days ago\"").isEmpty());
+    assertResultEquals(change2, queryOne("after:\"2 days ago\""));
+
+    List<ChangeInfo> results;
+    results = query("after:\"3 days ago\"");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+
+    results = query("after:\"72 hours ago\"");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
   protected ChangeInserter newChange(
       TestRepository<InMemoryRepository> repo,
       @Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,
@@ -750,7 +916,7 @@
     return results.get(0);
   }
 
-  private static long lastUpdatedMs(Change c) {
+  protected static long lastUpdatedMs(Change c) {
     return c.getLastUpdatedOn().getTime();
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java
new file mode 100644
index 0000000..948626e
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV7Test.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.testutil.InMemoryModule;
+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.junit.Test;
+
+import java.util.List;
+
+public class LuceneQueryChangesV7Test extends AbstractQueryChangesTest {
+  protected Injector createInjector() {
+    Config cfg = InMemoryModule.newDefaultConfig();
+    cfg.setInt("index", "lucene", "testVersion", 7);
+    return Guice.createInjector(new InMemoryModule(cfg));
+  }
+
+  @Test
+  public void pagination() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    List<Change> changes = Lists.newArrayList();
+    for (int i = 0; i < 5; i++) {
+      changes.add(newChange(repo, null, null, null, null).insert());
+    }
+
+    // Page forward and back through 3 pages of results.
+    QueryChanges q;
+    List<ChangeInfo> results;
+    results = query("status:new limit:2");
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(4), results.get(0));
+    assertResultEquals(changes.get(3), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyBefore(results.get(1)._sortkey);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(2), results.get(0));
+    assertResultEquals(changes.get(1), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyBefore(results.get(1)._sortkey);
+    results = query(q);
+    assertEquals(1, results.size());
+    assertResultEquals(changes.get(0), results.get(0));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyAfter(results.get(0)._sortkey);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(2), results.get(0));
+    assertResultEquals(changes.get(1), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyAfter(results.get(0)._sortkey);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(4), results.get(0));
+    assertResultEquals(changes.get(3), results.get(1));
+  }
+
+  @Override
+  @Test
+  public void updatedOrderWithSubMinuteResolution() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+
+    assertTrue(lastUpdatedMs(change1) < lastUpdatedMs(change2));
+
+    List<ChangeInfo> results;
+    results = query("status:new");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+
+    ReviewInput input = new ReviewInput();
+    input.message = "toplevel";
+    postReview.apply(new RevisionResource(
+        changes.parse(change1.getId()), ins1.getPatchSet()), input);
+    change1 = db.changes().get(change1.getId());
+
+    assertTrue(lastUpdatedMs(change1) > lastUpdatedMs(change2));
+    assertTrue(lastUpdatedMs(change1) - lastUpdatedMs(change2)
+        < MILLISECONDS.convert(1, MINUTES));
+
+    results = query("status:new");
+    assertEquals(2, results.size());
+    // Same order as before change1 was modified.
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void sortKeyBreaksTiesOnChangeId() throws Exception {
+    clockStepMs = 0;
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+
+    ReviewInput input = new ReviewInput();
+    input.message = "toplevel";
+    postReview.apply(new RevisionResource(
+        changes.parse(change1.getId()), ins1.getPatchSet()), input);
+    change1 = db.changes().get(change1.getId());
+
+    assertEquals(change1.getLastUpdatedOn(), change2.getLastUpdatedOn());
+
+    List<ChangeInfo> results = query("status:new");
+    assertEquals(2, results.size());
+    // Updated at the same time, 2 > 1.
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+}
diff --git a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
index cf9c733..8c8b007 100644
--- a/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
+++ b/gerrit-solr/src/main/java/com/google/gerrit/solr/SolrChangeIndex.java
@@ -20,6 +20,7 @@
 import static com.google.gerrit.solr.IndexVersionCheck.solrIndexConfig;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -41,6 +42,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SortKeyPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
@@ -50,6 +52,7 @@
 import org.apache.lucene.search.Query;
 import org.apache.lucene.util.Version;
 import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.SolrQuery.SortClause;
 import org.apache.solr.client.solrj.SolrServer;
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.impl.CloudSolrServer;
@@ -100,9 +103,9 @@
     this.indexes = indexes;
     this.schema = schema;
 
-    String url = cfg.getString("index", "solr", "url");
+    String url = cfg.getString("index", null, "url");
     if (Strings.isNullOrEmpty(url)) {
-      throw new IllegalStateException("index.solr.url must be supplied");
+      throw new IllegalStateException("index.url must be supplied");
     }
 
     // Version is only used to determine the list of stop words used by the
@@ -210,7 +213,7 @@
   }
 
   @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+  public ChangeDataSource getSource(Predicate<ChangeData> p, int start, int limit)
       throws QueryParseException {
     Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
     List<SolrServer> indexes = Lists.newArrayListWithCapacity(2);
@@ -220,8 +223,24 @@
     if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
       indexes.add(closedIndex);
     }
-    return new QuerySource(indexes, queryBuilder.toQuery(p), limit,
-        ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
+    return new QuerySource(indexes, queryBuilder.toQuery(p), start, limit,
+        getSorts(schema, p));
+  }
+
+  @SuppressWarnings("deprecation")
+  private static List<SortClause> getSorts(Schema<ChangeData> schema,
+      Predicate<ChangeData> p) {
+    if (SortKeyPredicate.hasSortKeyField(schema)) {
+      boolean reverse = ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p);
+      return ImmutableList.of(new SortClause(ChangeField.SORTKEY.getName(),
+          !reverse ? SolrQuery.ORDER.desc : SolrQuery.ORDER.asc));
+    } else {
+      return ImmutableList.of(
+          new SortClause(
+            ChangeField.UPDATED.getName(), SolrQuery.ORDER.desc),
+          new SortClause(
+            ChangeField.LEGACY_ID.getName(), SolrQuery.ORDER.desc));
+    }
   }
 
   private void commit(SolrServer server) throws IOException {
@@ -236,17 +255,18 @@
     private final List<SolrServer> indexes;
     private final SolrQuery query;
 
-    public QuerySource(List<SolrServer> indexes, Query q, int limit,
-        boolean reverse) {
+    public QuerySource(List<SolrServer> indexes, Query q, int start, int limit,
+        List<SortClause> sorts) {
       this.indexes = indexes;
 
       query = new SolrQuery(q.toString());
       query.setParam("shards.tolerant", true);
       query.setParam("rows", Integer.toString(limit));
+      if (start != 0) {
+        query.setParam("start", Integer.toString(start));
+      }
       query.setFields(ID_FIELD);
-      query.setSort(
-          ChangeField.SORTKEY.getName(),
-          !reverse ? SolrQuery.ORDER.desc : SolrQuery.ORDER.asc);
+      query.setSorts(sorts);
     }
 
     @Override
@@ -329,8 +349,17 @@
         doc.addField(name, (Long) value);
       }
     } else if (type == FieldType.TIMESTAMP) {
-      for (Object v : values.getValues()) {
-        doc.addField(name, QueryBuilder.toIndexTime((Timestamp) v));
+      @SuppressWarnings("deprecation")
+      boolean legacy = values.getField() == ChangeField.LEGACY_UPDATED;
+      if (legacy) {
+        for (Object value : values.getValues()) {
+          int t = queryBuilder.toIndexTimeInMinutes((Timestamp) value);
+          doc.addField(name, t);
+        }
+      } else {
+        for (Object value : values.getValues()) {
+          doc.addField(name, ((Timestamp) value).getTime());
+        }
       }
     } else if (type == FieldType.EXACT
         || type == FieldType.PREFIX
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
index e0fe7de..7fb9226 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
@@ -21,11 +21,19 @@
 import java.lang.annotation.Target;
 
 /**
- * Annotation tagged on a concrete Command to describe what it is doing
+ * Annotation tagged on a concrete Command to describe what it is doing and
+ * whether it can be run on slaves.
  */
-@Target( {ElementType.TYPE})
+@Target({ElementType.TYPE})
 @Retention(RUNTIME)
 public @interface CommandMetaData {
+  public enum Mode {
+    MASTER, MASTER_OR_SLAVE;
+    public boolean isSupported(boolean slaveMode) {
+      return this == MASTER_OR_SLAVE || !slaveMode;
+    }
+  }
   String name();
   String description() default "";
+  Mode runsAt() default Mode.MASTER;
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
index bfa4051..1e409d2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
@@ -21,6 +21,8 @@
 
 /** Module to register commands in the SSH daemon. */
 public abstract class CommandModule extends LifecycleModule {
+  protected boolean slaveMode;
+
   /**
    * Configure a command to be invoked by name.
    *
@@ -74,7 +76,9 @@
     if (meta == null) {
       throw new IllegalStateException("no CommandMetaData annotation found");
     }
-    bind(Commands.key(parent, meta.name(), meta.description())).to(clazz);
+    if (meta.runsAt().isSupported(slaveMode)) {
+      bind(Commands.key(parent, meta.name(), meta.description())).to(clazz);
+    }
   }
 
   /**
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 8dc3f2c..40e58f2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -140,14 +140,13 @@
       }
     }
 
-    if (!SshUtil.createUser(sd, userFactory, key.getAccount())
-        .getAccount().isActive()) {
+    IdentifiedUser cu = SshUtil.createUser(sd, userFactory, key.getAccount());
+    if (!cu.getAccount().isActive()) {
       sd.authenticationError(username, "inactive-account");
       return false;
     }
 
-    return SshUtil.success(username, session, sshScope, sshLog, sd,
-        SshUtil.createUser(sd, userFactory, key.getAccount()));
+    return SshUtil.success(username, session, sshScope, sshLog, sd, cu);
   }
 
   private Set<PublicKey> getPeerKeys() {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index d548d34..fa5ab53 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityUtils;
 import com.google.gerrit.server.args4j.SubcommandHandler;
-import com.google.gerrit.sshd.commands.ErrorSlaveMode;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -155,13 +154,9 @@
     String format = "%-" + maxLength + "s   %s";
     for (String name : Sets.newTreeSet(commands.keySet())) {
       final CommandProvider p = commands.get(name);
-      Command c = p.getProvider().get();
-      String description = c instanceof ErrorSlaveMode
-          ? "Command disabled: server is running in slave mode"
-          : Strings.nullToEmpty(p.getDescription());
-
       usage.append("   ");
-      usage.append(String.format(format, name, description));
+      usage.append(String.format(format, name,
+          Strings.nullToEmpty(p.getDescription())));
       usage.append("\n");
     }
     usage.append("\n");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 2c5499a..50ab639 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.sshd.commands.DefaultCommandModule;
 import com.google.gerrit.sshd.commands.QueryShell;
 import com.google.inject.Inject;
 import com.google.inject.internal.UniqueAnnotations;
@@ -84,8 +83,6 @@
     bind(GSSAuthenticator.class).to(GerritGSSAuthenticator.class);
     bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
 
-    install(new DefaultCommandModule());
-
     bind(ModuleGenerator.class).to(SshAutoRegisterModuleGenerator.class);
     bind(SshPluginStarterCallback.class);
     bind(StartPluginListener.class)
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java
index a27027c..0aa12c4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AproposCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.documentation.QueryDocumentationExecutor;
 import com.google.gerrit.server.documentation.QueryDocumentationExecutor.DocQueryException;
@@ -26,7 +28,8 @@
 
 import java.util.List;
 
-@CommandMetaData(name = "apropos", description = "Search in Gerrit documentation")
+@CommandMetaData(name = "apropos", description = "Search in Gerrit documentation",
+  runsAt = MASTER_OR_SLAVE)
 final class AproposCommand extends SshCommand {
   @Inject
   private QueryDocumentationExecutor searcher;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index dc22a29..1d4b900 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.BanCommitResult;
@@ -33,7 +35,8 @@
 import java.util.ArrayList;
 import java.util.List;
 
-@CommandMetaData(name = "ban-commit", description = "Ban a commit from a project's repository")
+@CommandMetaData(name = "ban-commit", description = "Ban a commit from a project's repository",
+  runsAt = MASTER_OR_SLAVE)
 public class BanCommitCommand extends SshCommand {
   @Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
   private String reason;
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 75743b0..f905c5b 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
@@ -21,18 +21,18 @@
 import com.google.gerrit.sshd.SuExec;
 
 
-/** Register the basic commands any Gerrit server should support. */
+/** Register the commands a Gerrit server supports. */
 public class DefaultCommandModule extends CommandModule {
+  public DefaultCommandModule(boolean slave) {
+    slaveMode = slave;
+  }
+
   @Override
   protected void configure() {
     final CommandName git = Commands.named("git");
     final CommandName gerrit = Commands.named("gerrit");
     final CommandName plugin = Commands.named(gerrit, "plugin");
-
-    // The following commands can be ran on a server in either Master or Slave
-    // mode. If a command should only be used on a server in one mode, but not
-    // both, it should be bound in both MasterCommandModule and
-    // SlaveCommandModule.
+    final CommandName testSubmit = Commands.named(gerrit, "test-submit");
 
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
     command(gerrit, AproposCommand.class);
@@ -49,7 +49,6 @@
     command(gerrit, StreamEvents.class);
     command(gerrit, VersionCommand.class);
     command(gerrit, GarbageCollectionCommand.class);
-
     command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
 
     command(plugin, PluginLsCommand.class);
@@ -61,21 +60,43 @@
     alias(plugin, "rm", PluginRemoveCommand.class);
 
     command(git).toProvider(new DispatchCommandProvider(git));
-    command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
-    command(git, "upload-pack").to(Upload.class);
 
     command("ps").to(ShowQueue.class);
     command("kill").to(KillCommand.class);
     command("scp").to(ScpCommand.class);
 
     // Honor the legacy hyphenated forms as aliases for the non-hyphenated forms
-    //
     command("git-upload-pack").to(Commands.key(git, "upload-pack"));
-    command("git-receive-pack").to(Commands.key(git, "receive-pack"));
-    command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
-
+    command(git, "upload-pack").to(Upload.class);
     command("suexec").to(SuExec.class);
-
     listener().to(ShowCaches.StartupListener.class);
+
+    // The following commands can only be ran on a server in Master mode
+    command(gerrit, CreateAccountCommand.class);
+    command(gerrit, CreateGroupCommand.class);
+    command(gerrit, CreateProjectCommand.class);
+    command(gerrit, AdminQueryShell.class);
+    if (!slaveMode) {
+      command("git-receive-pack").to(Commands.key(git, "receive-pack"));
+      command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
+      command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
+      command(gerrit, "test-submit").toProvider(
+          new DispatchCommandProvider(testSubmit));
+    }
+    command(gerrit, Receive.class);
+
+    command(gerrit, RenameGroupCommand.class);
+    command(gerrit, ReviewCommand.class);
+    command(gerrit, SetProjectCommand.class);
+    command(gerrit, SetReviewersCommand.class);
+
+    command(gerrit, SetMembersCommand.class);
+    command(gerrit, CreateBranchCommand.class);
+    command(gerrit, SetAccountCommand.class);
+    command(gerrit, AdminSetParent.class);
+
+    command(gerrit, CreateAccountCommand.class);
+    command(testSubmit, TestSubmitRuleCommand.class);
+    command(testSubmit, TestSubmitTypeCommand.class);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ErrorSlaveMode.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ErrorSlaveMode.java
deleted file mode 100644
index 32c72038..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ErrorSlaveMode.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.sshd.BaseCommand;
-
-import org.apache.sshd.server.Environment;
-
-import java.io.IOException;
-
-/**
- * A command which just throws an error because it shouldn't be ran on this
- * server. This is used when a user tries to run a command on a server in Slave
- * Mode, but the command only applies to the Master server.
- */
-public final class ErrorSlaveMode extends BaseCommand {
-  @Override
-  public void start(final Environment env) {
-    String msg =
-        "error: That command is disabled on this server.\n\n"
-            + "Please use the master server URL.\n";
-    try {
-      err.write(msg.getBytes(ENC));
-      err.flush();
-    } catch (IOException e) {
-      // Ignore errors writing to the client
-    }
-    onExit(1);
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 6c07ddd..40152b0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.common.cache.Cache;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -31,7 +33,8 @@
 
 /** Causes the caches to purge all entries and reload. */
 @RequiresCapability(GlobalCapability.FLUSH_CACHES)
-@CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory")
+@CommandMetaData(name = "flush-caches", description = "Flush some/all server caches from memory",
+  runsAt = MASTER_OR_SLAVE)
 final class FlushCaches extends CacheCommand {
   private static final String WEB_SESSIONS = "web_sessions";
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
index 81af0d8..821701d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.GarbageCollectionResult;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -37,7 +39,8 @@
 
 /** Runs the Git garbage collection. */
 @RequiresCapability(GlobalCapability.RUN_GC)
-@CommandMetaData(name = "gc", description = "Run Git garbage collection")
+@CommandMetaData(name = "gc", description = "Run Git garbage collection",
+  runsAt = MASTER_OR_SLAVE)
 public class GarbageCollectionCommand extends BaseCommand {
 
   @Option(name = "--all", usage = "runs the Git garbage collection for all projects")
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index aadb1d9..f0169a8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.Url;
@@ -37,7 +39,8 @@
 
 import java.io.PrintWriter;
 
-@CommandMetaData(name = "ls-groups", description = "List groups visible to the caller")
+@CommandMetaData(name = "ls-groups", description = "List groups visible to the caller",
+  runsAt = MASTER_OR_SLAVE)
 public class ListGroupsCommand extends BaseCommand {
   @Inject
   private MyListGroups impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
index b7dd380..2f367de 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListMembersCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
@@ -39,7 +41,8 @@
 /**
  * Implements a command that allows the user to see the members of a group.
  */
-@CommandMetaData(name = "ls-members", description = "Lists the members of a given group")
+@CommandMetaData(name = "ls-members", description = "Lists the members of a given group",
+  runsAt = MASTER_OR_SLAVE)
 public class ListMembersCommand extends BaseCommand {
   @Inject
   ListMembersCommandImpl impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index 8bcae4b..78034fc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.server.project.ListProjects;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -23,7 +25,8 @@
 
 import java.util.List;
 
-@CommandMetaData(name = "ls-projects", description = "List projects visible to the caller")
+@CommandMetaData(name = "ls-projects", description = "List projects visible to the caller",
+  runsAt = MASTER_OR_SLAVE)
 final class ListProjectsCommand extends BaseCommand {
   @Inject
   private ListProjects impl;
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 207e5c2..c41fcdc 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
 import com.google.gerrit.common.data.GlobalCapability;
@@ -42,7 +43,8 @@
 import java.util.Map;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "ls-user-refs", description = "List refs visible to a specific user")
+@CommandMetaData(name = "ls-user-refs", description = "List refs visible to a specific user",
+  runsAt = MASTER_OR_SLAVE)
 public class LsUserRefs extends SshCommand {
   @Inject
   private AccountResolver accountResolver;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
deleted file mode 100644
index a79e862..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.sshd.CommandModule;
-import com.google.gerrit.sshd.CommandName;
-import com.google.gerrit.sshd.Commands;
-import com.google.gerrit.sshd.DispatchCommandProvider;
-
-
-/** Register the commands a Gerrit server in master mode supports. */
-public class MasterCommandModule extends CommandModule {
-  @Override
-  protected void configure() {
-    final CommandName gerrit = Commands.named("gerrit");
-    final CommandName testSubmit = Commands.named(gerrit, "test-submit");
-
-    command(gerrit, CreateAccountCommand.class);
-    command(gerrit, CreateGroupCommand.class);
-    command(gerrit, RenameGroupCommand.class);
-    command(gerrit, CreateProjectCommand.class);
-    command(gerrit, CreateBranchCommand.class);
-    command(gerrit, AdminQueryShell.class);
-    command(gerrit, SetReviewersCommand.class);
-    command(gerrit, Receive.class);
-    command(gerrit, AdminSetParent.class);
-    command(gerrit, ReviewCommand.class);
-    command(gerrit, SetAccountCommand.class);
-    command(gerrit, SetMembersCommand.class);
-    command(gerrit, SetProjectCommand.class);
-
-    command(gerrit, "test-submit").toProvider(new DispatchCommandProvider(testSubmit));
-    command(testSubmit, TestSubmitRuleCommand.class);
-    command(testSubmit, TestSubmitTypeCommand.class);
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
index 47c2d68..b2afde1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -28,7 +30,8 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "enable", description = "Enable plugins")
+@CommandMetaData(name = "enable", description = "Enable plugins",
+  runsAt = MASTER_OR_SLAVE)
 final class PluginEnableCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin(s) to enable")
   List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index 1e8035d..71f1517 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.common.base.Strings;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -35,7 +37,8 @@
 import java.net.URL;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "install", description = "Install/Add a plugin")
+@CommandMetaData(name = "install", description = "Install/Add a plugin",
+  runsAt = MASTER_OR_SLAVE)
 final class PluginInstallCommand extends SshCommand {
   @Option(name = "--name", aliases = {"-n"}, usage = "install under name")
   private String name;
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 7e44641..9f6bb50 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
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.ListPlugins;
@@ -26,7 +28,8 @@
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "ls", description = "List the installed plugins")
+@CommandMetaData(name = "ls", description = "List the installed plugins",
+  runsAt = MASTER_OR_SLAVE)
 final class PluginLsCommand extends BaseCommand {
   @Inject
   private ListPlugins impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
index 3ed1011..8449160 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -14,10 +14,12 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.InvalidPluginException;
 import com.google.gerrit.server.plugins.PluginInstallException;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginLoader;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -28,7 +30,8 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "reload", description = "Reload/Restart plugins")
+@CommandMetaData(name = "reload", description = "Reload/Restart plugins",
+  runsAt = MASTER_OR_SLAVE)
 final class PluginReloadCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
   private List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
index 0ae11af..757348f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -27,7 +29,8 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-@CommandMetaData(name = "remove", description = "Disable plugins")
+@CommandMetaData(name = "remove", description = "Disable plugins",
+  runsAt = MASTER_OR_SLAVE)
 final class PluginRemoveCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin to remove")
   List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index af42e1b..2bda15d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.server.query.change.QueryProcessor;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -24,7 +26,8 @@
 
 import java.util.List;
 
-@CommandMetaData(name = "query", description = "Query the change database")
+@CommandMetaData(name = "query", description = "Query the change database",
+  runsAt = MASTER_OR_SLAVE)
 class Query extends SshCommand {
   @Inject
   private QueryProcessor processor;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index ff1de80..397120f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheStats;
 import com.google.common.collect.Maps;
@@ -54,7 +56,8 @@
 
 /** Show the current cache states. */
 @RequiresCapability(GlobalCapability.VIEW_CACHES)
-@CommandMetaData(name = "show-caches", description = "Display current cache statistics")
+@CommandMetaData(name = "show-caches", description = "Display current cache statistics",
+  runsAt = MASTER_OR_SLAVE)
 final class ShowCaches extends CacheCommand {
   private static volatile long serverStarted;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index d97d750..17bea45 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.CurrentUser;
@@ -46,7 +48,8 @@
 
 /** Show the current SSH connections. */
 @RequiresCapability(GlobalCapability.VIEW_CONNECTIONS)
-@CommandMetaData(name = "show-connections", description = "Display active client SSH connections")
+@CommandMetaData(name = "show-connections", description = "Display active client SSH connections",
+  runsAt = MASTER_OR_SLAVE)
 final class ShowConnections extends SshCommand {
   @Option(name = "--numeric", aliases = {"-n"}, usage = "don't resolve names")
   private boolean numeric;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index afb5787..40f7059 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.TaskInfoFactory;
@@ -42,7 +44,8 @@
 
 /** Display the current work queue. */
 @AdminHighPriorityCommand
-@CommandMetaData(name = "show-queue", description = "Display the background work queues")
+@CommandMetaData(name = "show-queue", description = "Display the background work queues",
+  runsAt = MASTER_OR_SLAVE)
 final class ShowQueue extends SshCommand {
   @Option(name = "--wide", aliases = {"-w"}, usage = "display without line width truncation")
   private boolean wide;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
deleted file mode 100644
index ac0eb0d..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.sshd.CommandModule;
-import com.google.gerrit.sshd.CommandName;
-import com.google.gerrit.sshd.Commands;
-
-
-/** Register the commands a Gerrit server in slave mode supports. */
-public class SlaveCommandModule extends CommandModule {
-  @Override
-  protected void configure() {
-    final CommandName gerrit = Commands.named("gerrit");
-
-    command(gerrit, "create-account").to(ErrorSlaveMode.class);
-    command(gerrit, "create-group").to(ErrorSlaveMode.class);
-    command(gerrit, "create-project").to(ErrorSlaveMode.class);
-    command(gerrit, "gsql").to(ErrorSlaveMode.class);
-    command(gerrit, "receive-pack").to(ErrorSlaveMode.class);
-    command(gerrit, "rename-group").to(ErrorSlaveMode.class);
-    command(gerrit, "review").to(ErrorSlaveMode.class);
-    command(gerrit, "set-project-parent").to(ErrorSlaveMode.class);
-    command(gerrit, "set-reviewers").to(ErrorSlaveMode.class);
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
index fd4a9ec..c55c7ed2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.ChangeListener;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -36,7 +38,8 @@
 import java.util.concurrent.LinkedBlockingQueue;
 
 @RequiresCapability(GlobalCapability.STREAM_EVENTS)
-@CommandMetaData(name = "stream-events", description = "Monitor events occurring in real time")
+@CommandMetaData(name = "stream-events", description = "Monitor events occurring in real time",
+  runsAt = MASTER_OR_SLAVE)
 final class StreamEvents extends BaseCommand {
   /** Maximum number of events that may be queued up for each connection. */
   private static final int MAX_EVENTS = 128;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
index 19888c8..50f880d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.sshd.commands;
 
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
 import com.google.gerrit.common.Version;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 
-@CommandMetaData(name = "version", description = "Display gerrit version")
+@CommandMetaData(name = "version", description = "Display gerrit version",
+  runsAt = MASTER_OR_SLAVE)
 final class VersionCommand extends SshCommand {
 
   @Override
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 32fac33..45cc4a2a 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
@@ -57,7 +57,7 @@
 import com.google.gerrit.sshd.SshHostKeyModule;
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
-import com.google.gerrit.sshd.commands.MasterCommandModule;
+import com.google.gerrit.sshd.commands.DefaultCommandModule;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
@@ -304,7 +304,7 @@
     final List<Module> modules = new ArrayList<Module>();
     modules.add(sysInjector.getInstance(SshModule.class));
     modules.add(new SshHostKeyModule());
-    modules.add(new MasterCommandModule());
+    modules.add(new DefaultCommandModule(false));
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/lib/maven.defs b/lib/maven.defs
index 09fd1a2..b874d8e 100644
--- a/lib/maven.defs
+++ b/lib/maven.defs
@@ -124,3 +124,37 @@
       source_jar = genfile(srcjar) if srcjar else None,
       visibility = visibility,
     )
+
+def local_jar(
+    name,
+    jar,
+    src = None,
+    deps = [],
+    visibility = ['PUBLIC']):
+  binjar = name + '.jar'
+  srcjar = name + '-src.jar'
+  genrule(
+    name = name + '__local_bin',
+    cmd = 'ln -s %s $OUT' % jar,
+    out = binjar)
+  if src:
+    genrule(
+      name = name + '__local_src',
+      cmd = 'ln -s %s $OUT' % src,
+      out = srcjar)
+    prebuilt_jar(
+      name = name + '_src',
+      deps = [':' + name + '__local_src'],
+      binary_jar = genfile(srcjar),
+      visibility = visibility,
+    )
+  else:
+    srcjar = None
+
+  prebuilt_jar(
+    name = name,
+    deps = deps + [':' + name + '__local_bin'],
+    binary_jar = genfile(binjar),
+    source_jar = genfile(srcjar) if srcjar else None,
+    visibility = visibility,
+  )
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index a4aaa5a..250e89f 100644
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -23,6 +23,9 @@
 def mvn(action):
   return ['mvn', '--file', path.join(self, 'fake_pom_%s.xml' % action)]
 
+def mvn(action):
+  return ['mvn', '--file', path.join(self, 'fake_pom_%s.xml' % action)]
+
 opts = OptionParser()
 opts.add_option('--repository', help='maven repository id')
 opts.add_option('--url', help='maven repository url')