Merge "Support to inherit project level plugin configuration"
diff --git a/.buckconfig b/.buckconfig
index 171acb5..1bc29ac 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -3,11 +3,7 @@
   api_deploy = //tools/maven:deploy
   api_install = //tools/maven:install
   docs = //Documentation:html
-  download = //tools:download
-  download_sources = //tools:download_sources
   gerrit = //:gerrit
-  eclipse = //tools/eclipse:eclipse
-  eclipse_project = //tools/eclipse:eclipse_project
   release = //:release
 
 [buildfile]
diff --git a/.buckversion b/.buckversion
index 4b453f2..ca3c391 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-5fc60079d9dbaaf8a1e7d542dcb21fd901f68245
+4464beabf38ba9c329918b9c7391064bdb2d3fe9
diff --git a/BUCK b/BUCK
index b1a0c75..616a0fe 100644
--- a/BUCK
+++ b/BUCK
@@ -15,9 +15,12 @@
 
 genrule(
   name = 'api',
-  cmd = '',
+  cmd = ';'.join(
+    ['cd $TMP'] +
+    ['ln -s $(location %s) .' % n for n in API_DEPS] +
+    ['zip -q0 $OUT *']),
   deps = API_DEPS,
-  out = '__fake.api__',
+  out = 'api.zip',
 )
 
 java_binary(
diff --git a/Documentation/BUCK b/Documentation/BUCK
index faa2553..2876f13 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -4,48 +4,41 @@
 
 MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
 SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
-HTML = [txt[0:-4] + '.html' for txt in SRCS]
 
 genrule(
   name = 'html',
   cmd = 'cd $TMP;' +
     'mkdir -p Documentation/images;' +
+    'unzip -q $SRCDIR/only_html.zip -d Documentation/;' +
     'for s in $SRCS;do ln -s $s Documentation;done;' +
     'mv Documentation/*.{jpg,png} Documentation/images;' +
+    'rm Documentation/only_html.zip;' +
     'rm Documentation/licenses.txt;' +
     'cp $SRCDIR/licenses.txt LICENSES.txt;' +
     'zip -qr $OUT *',
-  srcs = [genfile(d) for d in HTML] +
+  srcs = [genfile('only_html.zip')] +
     glob([
       'images/*.jpg',
       'images/*.png',
     ]) + [
-    genfile('doc.css'),
-    genfile('licenses.html'),
+    'doc.css',
     genfile('licenses.txt'),
   ],
-  deps = [':' + d for d in HTML] + [
-    ':licenses.html',
+  deps = [
+    ':generate_html',
     ':licenses.txt',
-    ':doc.css',
   ],
   out = 'html.zip',
   visibility = ['PUBLIC'],
 )
 
-genrule(
-  name = 'doc.css',
-  cmd = 'ln -s $SRCDIR/doc.css $OUT',
-  srcs = ['doc.css'],
-  out = 'doc.css',
-)
-
 genasciidoc(
+  name = 'generate_html',
   srcs = SRCS + [genfile('licenses.txt')],
-  outs = HTML + ['licenses.html'],
-  deps = DOCUMENTATION_DEPS,
+  deps = [':licenses.txt'],
   attributes = documentation_attributes(git_describe()),
   backend = 'html5',
+  out = 'only_html.zip',
 )
 
 genrule(
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index 8279847..e2de785 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -13,25 +13,30 @@
 # limitations under the License.
 
 def genasciidoc(
+    name,
+    out,
     srcs = [],
-    outs = [],
-    deps = {},
+    deps = [],
     attributes = [],
     backend = None,
     visibility = []):
   EXPN = '.expn'
 
-  asciidoc = ['$(exe //lib/asciidoctor:asciidoc)']
+  asciidoc = [
+      '$(exe //lib/asciidoctor:asciidoc)',
+      '-z', '$OUT',
+      '--in-ext', '".txt%s"' % EXPN,
+      '--out-ext', '".html"',
+  ]
   if backend:
     asciidoc.extend(['-b', backend])
   for attribute in attributes:
     asciidoc.extend(['-a', attribute])
-  asciidoc.extend(['-o', '$OUT'])
+  asciidoc.append('$SRCS')
+  newsrcs = []
+  newdeps = deps + ['//lib/asciidoctor:asciidoc']
 
-  for p in zip(srcs, outs):
-    src, out = p
-    dep = deps.get(src) or []
-
+  for src in srcs:
     tx = []
     fn = src
     if fn.startswith('BUCKGEN:') :
@@ -48,14 +53,14 @@
       deps = tx + [':replace_macros'],
       out = ex,
     )
-    genrule(
-      name = out,
-      cmd = ' '.join(asciidoc + ['$SRCDIR/' + ex]),
-      srcs = [genfile(ex)] + [genfile(n + EXPN) for n in dep],
-      deps = [':' + n + EXPN for n in dep] + [
-        ':' + ex,
-        '//lib/asciidoctor:asciidoc',
-      ],
-      out = out,
-      visibility = visibility,
-    )
+    newdeps.append(':' + ex)
+    newsrcs.append(genfile(ex))
+
+  genrule(
+    name = name,
+    cmd = ' '.join(asciidoc),
+    srcs = newsrcs,
+    deps = newdeps,
+    out = out,
+    visibility = visibility,
+  )
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 339b0c0..19749a3 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1334,6 +1334,7 @@
 ----
 fetch-cmd some://url/to/commit-msg .git/hooks/commit-msg ; chmod +x .git/hooks/commit-msg
 ----
+
 +
 By default unset; falls back to using scp from the canonical SSH host,
 or curl from the canonical HTTP URL for the server.  Only necessary if a
@@ -1357,6 +1358,11 @@
 Code Review's own bug tracker but could be directed to the system
 administrator's ticket queue.
 
+[[gerrit.changeScreen]]gerrit.changeScreen::
++
+Default change screen UI to direct users to. Valid values are
+`OLD_UI` and `CHANGE_SCREEN2`. Default is `OLD_UI`.
+
 [[gitweb]]Section gitweb
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index 74f56b3..a41a821 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -53,10 +53,10 @@
 Generating the Eclipse Project
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Create the Eclipse project by building the `eclipse` target:
+Create the Eclipse project:
 
 ----
-  buck build eclipse
+  tools/eclipse/project.py
 ----
 
 In Eclipse, choose 'Import existing project' and select the `gerrit` project
@@ -65,21 +65,20 @@
 Expand the `gerrit` project, right-click on the `buck-out` folder, select
 'Properties', and then under 'Attributes' check 'Derived'.
 
-Note that if you make any changes in the project configuration that get
-saved to the `.project` file, for example adding Resource Filters on a
-folder, they will be overwritten the next time you run `buck build eclipse`.
+Note that if you make any changes in the project configuration
+that get saved to the `.project` file, for example adding Resource
+Filters on a folder, they will be overwritten the next time you run
+`tools/eclipse/project.py`.
 
 
 Refreshing the Classpath
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
-Normally `buck build eclipse` does everything necessary to generate a working Eclipse
-environment, but if the code doesn't compile and an updated classpath is needed, the
-Eclipse project can be refreshed and missing dependency JARs can be downloaded by
-building the `eclipse_project` and `download` targets:
+If an updated classpath is needed, the Eclipse project can be
+refreshed and missing dependency JARs can be downloaded:
 
 ----
-  buck build eclipse_project download
+  tools/eclipse/project.py
 ----
 
 
@@ -92,7 +91,7 @@
 show documentation or dive into the implementation of a library JAR:
 
 ----
-  buck build download_sources
+  tools/eclipse/project.py --src
 ----
 
 
@@ -250,12 +249,10 @@
 
 Dependency JARs are normally downloaded automatically, but Buck can inspect
 its graph and download any missing JAR files.  This is useful to enable
-subsequent builds to run without network access.
-
-Force a download of dependency JARs by building the `download` target:
+subsequent builds to run without network access:
 
 ----
-  buck build download
+  tools/download_all.py
 ----
 
 When downloading from behind a proxy (which is common in some corporate
@@ -335,6 +332,31 @@
 [-] BUILDING...FINISHED 0.2s
 ----
 
+Overwrite Buck's settings
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the latest version of Buck the wrapper script `buck_common` will source
+one of these files (if they exist): /etc/buck.conf, $HOME/.buck/buck.conf or
+$HOME/.buckrc. The trivial case to overwrite the Buck's default 1GB heap size:
+
+----
+  cat > $HOME/.buckrc <<EOF
+  export BUCK_EXTRA_JAVA_ARGS="\
+  -XX:MaxPermSize=512m \
+  -Xms8000m \
+  -Xmx16000m"
+  EOF
+----
+
+Or to debug BUCK, set BUCK_DEBUG_MODE to anything non-empty, then connect to
+port 8888:
+
+----
+  cat > $HOME/.buckrc <<EOF
+  export BUCK_DEBUG_MODE="yes"
+  EOF
+----
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index cc96e81..5ab38b8 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -697,6 +697,41 @@
   "Bonjour François from change 1, patch set 1!"
 ====
 
+A special case is to bind an endpoint without a view name.  This is
+particularly useful for DELETE requests:
+
+[source,java]
+----
+public class Module extends AbstractModule {
+  @Override
+  protected void configure() {
+    install(new RestApiModule() {
+      @Override
+      protected void configure() {
+        delete(PROJECT_KIND)
+            .to(DeleteProject.class);
+      }
+    });
+  }
+}
+----
+
+For a `UiAction` bound this way, a JS API function can be provided.
+
+Currently only one restriction exists: per plugin only one `UiAction`
+can be bound per resource without view name. To define a JS function
+for the `UiAction`, "/" must be used as the name:
+
+[source,javascript]
+----
+Gerrit.install(function(self) {
+  function onDeleteProject(c) {
+    [...]
+  }
+  self.onAction('project', '/', onDeleteProject);
+});
+----
+
 [[http]]
 HTTP Servlets
 -------------
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 65dd3a4..bc5d0bd 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1020,6 +1020,48 @@
   ]
 ----
 
+[[suggest-reviewers]]
+Suggest Reviewers
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/suggest_reviewers?q=J&n=5'
+
+Suggest the reviewers for a given query `q` and result limit `n`. If result
+limit is not passed, then the default 10 is used.
+
+As result a list of link:#suggested-reviewer-info[SuggestedReviewerInfo] entries is returned.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/suggest_reviewers?q=J HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#suggestedreviewer",
+      "account": {
+        "_account_id": 1000097,
+        "name": "Jane Roe",
+        "email": "jane.roe@example.com"
+      }
+    },
+    {
+      "kind": "gerritcodereview#suggestedreviewer",
+      "group": {
+        "id": "4fd581c0657268f2bdcc26699fbf9ddb76e3a279",
+        "name": "Joiner"
+      }
+    }
+  ]
+----
+
 [[get-reviewer]]
 Get Reviewer
 ~~~~~~~~~~~~
@@ -2540,6 +2582,18 @@
 The time and date describing when the approval was made.
 |===========================
 
+[[group-base-info]]
+GroupBaseInfo
+~~~~~~~~~~~~~
+The `GroupBaseInfo` entity contains base information about the group.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name    |Description
+|`id`          |The id of the group.
+|`name`        |The name of the group.
+|==========================
+
 [[change-info]]
 ChangeInfo
 ~~~~~~~~~~
@@ -3118,6 +3172,17 @@
 to return results from the input rule.
 |===========================
 
+[[suggested-reviewer-info]]
+SuggestedReviewerInfo
+~~~~~~~~~~~~~~~~~~~~~
+The `SuggestedReviewerInfo` entity contains information about a reviewer
+that can be added to a change (an account or a group).
+
+`SuggestedReviewerInfo` has either the `account` field that contains
+the link:rest-api-accounts.html#account-info[AccountInfo] entity, or
+the `group` field that contains the
+link:rest-api-changes.html#group-base-info[GroupBaseInfo] entity.
+
 [[submit-info]]
 SubmitInfo
 ~~~~~~~~~~
diff --git a/ReleaseNotes/ReleaseNotes-2.6.2.txt b/ReleaseNotes/ReleaseNotes-2.6.2.txt
deleted file mode 100644
index af00a71..0000000
--- a/ReleaseNotes/ReleaseNotes-2.6.2.txt
+++ /dev/null
@@ -1,67 +0,0 @@
-Release notes for Gerrit 2.6.2
-==============================
-
-Gerrit 2.6.2 is now available:
-
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.6.2.war[
-https://gerrit-releases.storage.googleapis.com/gerrit-2.6.2.war]
-
-There are no schema changes from 2.6.1.
-
-However, if upgrading from a version older than 2.6, follow the upgrade
-procedure in the 2.6 link:ReleaseNotes-2.6.html[Release Notes].
-
-
-Bug Fixes
----------
-
-
-* Fix null-pointer exception when dashboard title is not specified.
-+
-If the title is not specified, the path of the dashboard config file
-is used as title.
-
-* Properly handle double-click on external group in GroupTable.
-+
-Double-clicking on an external group opens the group's URL (if it
-is provided).
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=1872[Issue 1872]:
-Fix tab expansion in diff screens when syntax coloring is on.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=1904[Issue 1904]:
-Fix diff screens for files with CRLF line endings.
-
-* Allow label values to be configured with no text.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=1966[Issue 1966]:
-Fix Gerrit plugins under Tomcat by avoiding Guice static filter.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=2010[Issue 2010]:
-Fix null-pointer exception when searching for changes with the query
-`owner:self`.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=2039[Issue 2039]:
-Fix browser null-pointer exception when ChangeCache is incomplete.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=2054[Issue 2054]:
-Expand capabilities of `ldap.groupMemberPattern`.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=2056[Issue 2056]:
-Display custom NoOp label score for open changes.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=2093[Issue 2093]:
-Fix incorrect title of "repo download" link on change screen.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=2098[Issue 2098]:
-Fix re-enabling of disabled plugins.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=2127[Issue 2127]:
-Remove hard-coded documentation links from the admin page.
-
-* link:https://code.google.com/p/gerrit/issues/detail?id=2128[Issue 2128]:
-Fix null-pointer exception when deleting draft patch set when previous
-draft was already deleted.
-
-No other changes since 2.6.1.
-
diff --git a/ReleaseNotes/ReleaseNotes-2.7.txt b/ReleaseNotes/ReleaseNotes-2.7.txt
index 348b812..3a2ebf9 100644
--- a/ReleaseNotes/ReleaseNotes-2.7.txt
+++ b/ReleaseNotes/ReleaseNotes-2.7.txt
@@ -4,12 +4,11 @@
 
 Gerrit 2.7 is now available:
 
-link:https://gerrit-releases.storage.googleapis.com/gerrit-2.7-rc2.war[https://gerrit-releases.storage.googleapis.com/gerrit-2.7-rc2.war]
+link:https://gerrit-releases.storage.googleapis.com/gerrit-2.7.war[
+https://gerrit-releases.storage.googleapis.com/gerrit-2.7.war]
 
-Gerrit 2.7 includes the bug fixes done with
-link:ReleaseNotes-2.6.1.html[Gerrit 2.6.1] and
-link:ReleaseNotes-2.6.2.html[Gerrit 2.6.2]. These bug fixes are *not*
-listed in these release notes.
+Gerrit 2.7 includes the bug fixes done with link:ReleaseNotes-2.6.1.html[Gerrit 2.6.1].
+These bug fixes are *not* listed in these release notes.
 
 Schema Change
 -------------
@@ -207,23 +206,49 @@
 
 * Postpone check for first account until adding an account.
 
-* Mark `ALREADY_MERGED` changes as merged in the database.
+* link:https://code.google.com/p/gerrit/issues/detail?id=1848[Issue 1848]:
+Mark `ALREADY_MERGED` changes as merged in the database.
 +
 If a change was marked `ALREADY_MERGED`, likely due to a bug in
 merge code, it does not end up in the list of changes to be submitted
 and never gets marked as merged despite the branch head already
 having advanced.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=600[Issue 600]:
+Fix change stuck in SUBMITTED state but actually merged.
 +
-Works around (but does not fix)
-link:https://code.google.com/p/gerrit/issues/detail?id=1985[Issue 1985] and
-link:https://code.google.com/p/gerrit/issues/detail?id=600[Issue 600]
-to allow recovery from certain kind of bad state.
+When submitting a commit that has a tag, it could not be merged.
+
+* Fix null-pointer exception when dashboard title is not specified.
++
+If the title is not specified, the path of the dashboard config file
+is used as title.
+
+* Allow label values to be configured with no text.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1966[Issue 1966]:
+Fix Gerrit plugins under Tomcat by avoiding Guice static filter.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2054[Issue 2054]:
+Expand capabilities of `ldap.groupMemberPattern`.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2098[Issue 2098]:
+Fix re-enabling of disabled plugins.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2128[Issue 2128]:
+Fix null-pointer exception when deleting draft patch set when previous
+draft was already deleted.
 
 
 Web UI
 ~~~~~~
 
 
+* Properly handle double-click on external group in GroupTable.
++
+Double-clicking on an external group opens the group's URL (if it
+is provided).
+
 * link:https://code.google.com/p/gerrit/issues/detail?id=1848[Issue 1848]:
 Don't discard inline comments when escape key is pressed.
 
@@ -234,6 +259,28 @@
 correct display of bold text on Mac OS X. Simplify the selector to sans-serif
 and allow the browser to use the user's preferred font in this family.
 
+* link:https://code.google.com/p/gerrit/issues/detail?id=1872[Issue 1872]:
+Fix tab expansion in diff screens when syntax coloring is on.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1904[Issue 1904]:
+Fix diff screens for files with CRLF line endings.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2056[Issue 2056]:
+Display custom NoOp label score for open changes.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2093[Issue 2093]:
+Fix incorrect title of "repo download" link on change screen.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2127[Issue 2127]:
+Remove hard-coded documentation links from the admin page.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2010[Issue 2010]:
+Fix null-pointer exception when searching for changes with the query
+`owner:self`.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2039[Issue 2039]:
+Fix browser null-pointer exception when ChangeCache is incomplete.
+
 
 REST API
 ~~~~~~~~
diff --git a/ReleaseNotes/ReleaseNotes-2.8.txt b/ReleaseNotes/ReleaseNotes-2.8.txt
index 1c63983f..c595b5b 100644
--- a/ReleaseNotes/ReleaseNotes-2.8.txt
+++ b/ReleaseNotes/ReleaseNotes-2.8.txt
@@ -21,23 +21,44 @@
 a later 2.1.x version), and then to 2.8.x.  If you are upgrading from 2.2.x.x or
 later, you may ignore this warning and upgrade directly to 2.8.x.
 
+*WARNING:* The replication plugin now automatically creates missing repositories
+on the destination if during the replication of a ref the target repository is
+found to be missing. This is a change in behavior of the replication plugin. To go
+back to the old behavior, set the parameter `remote.NAME.createMissingRepositories`
+in the `replication.config` file to `false`.
+
 
 Release Highlights
 ------------------
 
 
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config-gerrit.html#index[
+Secondary indexing with Lucene and Solr].
+
 * Lots of new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api.html[
 REST API endpoints].
 
-* New link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/js-api.html[
-JavaScript API].
+* New
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/dev-plugins.html#ui_extension[
+UI extension] and
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/js-api.html[
+JavaScript API] for plugins.
 
-* New build system using link:http://facebook.github.io/buck/[Facebook Buck].
+* New build system using Facebook's link:http://facebook.github.io/buck/[Buck].
 
 
 New Features
 ------------
 
+Build
+~~~~~
+
+* Gerrit is now built with
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/dev-buck.html[
+Buck].
+
+* Documentation is now built with Buck and link:http://asciidoctor.org[Asciidoctor].
+
 
 Configuration
 ~~~~~~~~~~~~~
@@ -79,6 +100,12 @@
 link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/database-setup.html#createdb_oracle[
 Oracle database].
 
+* New bash completion script for autocompletion of parameters to the gerrit.sh wrapper.
+
+* The site can be
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/config-auto-site-initialization.html[
+auto-initialized on server startup].
+
 Web UI
 ~~~~~~
 
@@ -219,6 +246,12 @@
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-changes.html#get-commit[
 Get parsed commit of a revision]
 
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-changes.html#publish-draft-change[
+Publish draft change]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/rest-api-changes.html#delete-draft-change[
+Delete draft change]
+
 
 Config
 ^^^^^^
@@ -297,6 +330,8 @@
 
 
 * Plugins may now contribute buttons to various parts of the UI using the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/dev-plugins.html#ui_extension[
+UI extension] and
 link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/js-api.html[
 JavaScript API].
 
@@ -313,9 +348,22 @@
 link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/dev-plugins.html#capabilities[
 Global capabilities].
 
+* Plugins may now
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/dev-plugins.html#plugin_name[
+define their own name].
+
 * The "hello world" plugin is replaced with the "cookbook plugin" which has more
 examples of the plugin API's usage.
 
+* Plugins may now trigger and listen to a "project deleted"
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.8/dev-plugins.html#events[
+event].
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=2101[Issue 2101]:
+Plugins implementing LifecycleListener can use auto registration.
+
+* Plugins may bind REST endpoints with empty view names.
+
 
 Commit Message Length Checker
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -326,6 +374,21 @@
 Replication
 ^^^^^^^^^^^
 
+* Automatically create missing repositories on the destination.
++
+If during the replication of a ref the target repository is found to be missing,
+the repository is automatically created.
++
+This is a change in behavior of the replication plugin. To go back to the old
+behavior, set the parameter `remote.NAME.createMissingRepositories` in the
+`replication.config` file to `false`.
+
+* Support for replication of project deletions.
++
+The replication plugin can now be configured to listen to project deletion events
+and to replicate the project deletions. By default project deletions are *not*
+replicated.
+
 * The `{$name}` placeholder is optional when replicating a single project,
 allowing a single project to be replicated under a different name.
 
@@ -344,6 +407,19 @@
 
 * The `startReplication` global capability is now provided by the plugin.
 
+* Pushes to each destination URI are serialized.
++
+Scheduling a retry to avoid collision with an in-flight push is differentiated
+from a retry due to a transport error.  In the case of collision avoidance, the
+job is rescheduled according to the replication delay, rather than the retry
+delay.
+
+Review Notes
+^^^^^^^^^^^^
+
+* Do not try to create review notes for ref deletion events.
+
+
 ssh
 ~~~
 
@@ -395,6 +471,12 @@
 * link:https://code.google.com/p/gerrit/issues/detail?id=600[Issue 600]:
 Fix change stuck in SUBMITTED state but actually merged.
 
+* link:https://code.google.com/p/gerrit/issues/detail?id=1699[Issue 1699]:
+Fix handling of projects with trailing ".git" suffix.
+
+* Limit retrying of submitted changes to 12 hours.
+
+
 Configuration
 ~~~~~~~~~~~~~
 
@@ -495,6 +577,8 @@
 
 * The `@CommandMetaData(descr)` annotation is deprecated in favor of `@CommandMetaData(description)`.
 
+* Improve the error message when rejecting upload for review to a read-only project.
+
 
 Emails
 ~~~~~~
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index c6c45eb..28b9f65 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -14,7 +14,6 @@
 [[2_6]]
 Version 2.6.x
 -------------
-* link:ReleaseNotes-2.6.2.html[2.6.2]
 * link:ReleaseNotes-2.6.1.html[2.6.1]
 * link:ReleaseNotes-2.6.html[2.6]
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
index 5f8faff..c17598f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
@@ -25,6 +25,7 @@
 import com.jcraft.jsch.Session;
 
 import org.eclipse.jgit.api.AddCommand;
+import org.eclipse.jgit.api.CheckoutCommand;
 import org.eclipse.jgit.api.CloneCommand;
 import org.eclipse.jgit.api.CommitCommand;
 import org.eclipse.jgit.api.Git;
@@ -33,6 +34,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.JschConfigSessionFactory;
@@ -130,47 +132,58 @@
     addCmd.call();
   }
 
-  public static String createCommit(Git git, PersonIdent i, String msg)
+  public static Commit createCommit(Git git, PersonIdent i, String msg)
       throws GitAPIException, IOException {
-    return createCommit(git, i, msg, true, false);
+    return createCommit(git, i, msg, null);
   }
 
-  public static void amendCommit(Git git, PersonIdent i, String msg, String changeId)
+  public static Commit amendCommit(Git git, PersonIdent i, String msg, String changeId)
       throws GitAPIException, IOException {
     msg = ChangeIdUtil.insertId(msg, ObjectId.fromString(changeId.substring(1)));
-    createCommit(git, i, msg, false, true);
+    return createCommit(git, i, msg, changeId);
   }
 
-  private static String createCommit(Git git, PersonIdent i, String msg,
-      boolean insertChangeId, boolean amend) throws GitAPIException, IOException {
-    ObjectId changeId = null;
-    if (insertChangeId) {
-      changeId = computeChangeId(git, i, msg);
-      msg = ChangeIdUtil.insertId(msg, changeId);
-    }
+  private static Commit createCommit(Git git, PersonIdent i, String msg,
+      String changeId) throws GitAPIException, IOException {
 
     final CommitCommand commitCmd = git.commit();
-    commitCmd.setAmend(amend);
+    commitCmd.setAmend(changeId != null);
     commitCmd.setAuthor(i);
     commitCmd.setCommitter(i);
-    commitCmd.setMessage(msg);
-    commitCmd.call();
 
-    return changeId != null ? "I" + changeId.getName() : null;
+    if (changeId == null) {
+      ObjectId id = computeChangeId(git, i, msg);
+      changeId = "I" + id.getName();
+    }
+    msg = ChangeIdUtil.insertId(msg, ObjectId.fromString(changeId.substring(1)));
+    commitCmd.setMessage(msg);
+
+    RevCommit c = commitCmd.call();
+    return new Commit(c, changeId);
   }
 
   private static ObjectId computeChangeId(Git git, PersonIdent i, String msg)
       throws IOException {
     RevWalk rw = new RevWalk(git.getRepository());
     try {
-      RevCommit parent =
-          rw.lookupCommit(git.getRepository().getRef(Constants.HEAD).getObjectId());
-      return ChangeIdUtil.computeChangeId(parent.getTree(), parent.getId(), i, i, msg);
+      Ref head = git.getRepository().getRef(Constants.HEAD);
+      if (head.getObjectId() != null) {
+        RevCommit parent = rw.lookupCommit(head.getObjectId());
+        return ChangeIdUtil.computeChangeId(parent.getTree(), parent.getId(), i, i, msg);
+      } else {
+        return ChangeIdUtil.computeChangeId(null, null, i, i, msg);
+      }
     } finally {
       rw.release();
     }
   }
 
+  public static void checkout(Git git, String name) throws GitAPIException {
+    CheckoutCommand checkout = git.checkout();
+    checkout.setName(name);
+    checkout.call();
+  }
+
   public static PushResult pushHead(Git git, String ref, boolean pushTags)
       throws GitAPIException {
     PushCommand pushCmd = git.push();
@@ -181,4 +194,22 @@
     Iterable<PushResult> r = pushCmd.call();
     return Iterables.getOnlyElement(r);
   }
+
+  public static class Commit {
+    private final RevCommit commit;
+    private final String changeId;
+
+    Commit(RevCommit commit, String changeId) {
+      this.commit = commit;
+      this.changeId = changeId;
+    }
+
+    public RevCommit getCommit() {
+      return commit;
+    }
+
+    public String getChangeId() {
+      return changeId;
+    }
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
index 7450565..1c0fafe 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushOneCommit.java
@@ -27,6 +27,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.GitUtil.Commit;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -36,7 +37,9 @@
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
@@ -82,15 +85,17 @@
   public Result to(Git git, String ref)
       throws GitAPIException, IOException {
     add(git, fileName, content);
+    Commit c;
     if (changeId != null) {
-      amendCommit(git, i, subject, changeId);
+      c = amendCommit(git, i, subject, changeId);
     } else {
-      changeId = createCommit(git, i, subject);
+      c = createCommit(git, i, subject);
+      changeId = c.getChangeId();
     }
     if (tagName != null) {
       git.tag().setName(tagName).setAnnotated(false).call();
     }
-    return new Result(db, ref, pushHead(git, ref, tagName != null), changeId, subject);
+    return new Result(db, ref, pushHead(git, ref, tagName != null), c, subject);
   }
 
   public void setTag(final String tagName) {
@@ -101,32 +106,40 @@
     private final ReviewDb db;
     private final String ref;
     private final PushResult result;
-    private final String changeId;
+    private final Commit commit;
     private final String subject;
 
-    private Result(ReviewDb db, String ref, PushResult result, String changeId,
+    private Result(ReviewDb db, String ref, PushResult result, Commit commit,
         String subject) {
       this.db = db;
       this.ref = ref;
       this.result = result;
-      this.changeId = changeId;
+      this.commit = commit;
       this.subject = subject;
     }
 
     public PatchSet.Id getPatchSetId() throws OrmException {
       return Iterables.getOnlyElement(
-          db.changes().byKey(new Change.Key(changeId))).currentPatchSetId();
+          db.changes().byKey(new Change.Key(commit.getChangeId()))).currentPatchSetId();
     }
 
     public String getChangeId() {
-      return changeId;
+      return commit.getChangeId();
+    }
+
+    public ObjectId getCommitId() {
+      return commit.getCommit().getId();
+    }
+
+    public RevCommit getCommit() {
+      return commit.getCommit();
     }
 
     public void assertChange(Change.Status expectedStatus,
         String expectedTopic, TestAccount... expectedReviewers)
         throws OrmException {
       Change c =
-          Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId)).toList());
+          Iterables.getOnlyElement(db.changes().byKey(new Change.Key(commit.getChangeId())).toList());
       assertEquals(subject, c.getSubject());
       assertEquals(expectedStatus, c.getStatus());
       assertEquals(expectedTopic, Strings.emptyToNull(c.getTopic()));
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
new file mode 100644
index 0000000..5209a0c
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -0,0 +1,256 @@
+// 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.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+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.acceptance.git.GitUtil;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+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.git.GitRepositoryManager;
+import com.google.gson.Gson;
+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.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public abstract class AbstractSubmit extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  private GitRepositoryManager repoManager;
+
+  protected RestSession session;
+
+  private TestAccount admin;
+  private Project.NameKey project;
+  private ReviewDb db;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.admin();
+    session = new RestSession(server, admin);
+    initSsh(admin);
+
+    project = new Project.NameKey("p");
+
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  protected abstract SubmitType getSubmitType();
+
+  @Test
+  public void submitToEmptyRepo() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject(false);
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    assertEquals(change.getCommitId(), getRemoteHead().getId());
+  }
+
+  protected Git createProject() throws JSchException, IOException,
+      GitAPIException {
+    return createProject(true);
+  }
+
+  private Git createProject(boolean emptyCommit)
+      throws JSchException, IOException, GitAPIException {
+    SshSession sshSession = new SshSession(server, admin);
+    try {
+      GitUtil.createProject(sshSession, project.get(), null, emptyCommit);
+      setSubmitType(getSubmitType());
+      return cloneProject(sshSession.getUrl() + "/" + project.get());
+    } finally {
+      sshSession.close();
+    }
+  }
+
+  private void setSubmitType(SubmitType submitType) throws IOException {
+    ProjectConfigInput in = new ProjectConfigInput();
+    in.submit_type = submitType;
+    in.use_content_merge = InheritableBoolean.FALSE;
+    RestResponse r = session.put("/projects/" + project.get() + "/config", in);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+  }
+
+  protected void setUseContentMerge() throws IOException {
+    ProjectConfigInput in = new ProjectConfigInput();
+    in.use_content_merge = InheritableBoolean.TRUE;
+    RestResponse r = session.put("/projects/" + project.get() + "/config", in);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+  }
+
+  protected PushOneCommit.Result createChange(Git git) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, admin.getIdent());
+    return push.to(git, "refs/for/master");
+  }
+
+  protected PushOneCommit.Result createChange(Git git, String subject,
+      String fileName, String content) throws GitAPIException, IOException {
+    PushOneCommit push =
+        new PushOneCommit(db, admin.getIdent(), subject, fileName, content);
+    return push.to(git, "refs/for/master");
+  }
+
+  protected void submit(String changeId) throws IOException {
+    submit(changeId, HttpStatus.SC_OK);
+  }
+
+  protected void submitWithConflict(String changeId) throws IOException {
+    submit(changeId, HttpStatus.SC_CONFLICT);
+  }
+
+  private void submit(String changeId, int expectedStatus) throws IOException {
+    approve(changeId);
+    RestResponse r =
+        session.post("/changes/" + changeId + "/submit",
+            SubmitInput.waitForMerge());
+    assertEquals(expectedStatus, r.getStatusCode());
+    if (expectedStatus == HttpStatus.SC_OK) {
+      ChangeInfo change =
+          (new Gson()).fromJson(r.getReader(),
+              new TypeToken<ChangeInfo>() {}.getType());
+      assertEquals("MERGED", change.status);
+    }
+    r.consume();
+  }
+
+  private void approve(String changeId) throws IOException {
+    RestResponse r =
+        session.post("/changes/" + changeId + "/revisions/current/review",
+            ReviewInput.approve());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+  }
+
+  protected void assertCherryPick(Git localGit, boolean contentMerge) throws IOException {
+    assertRebase(localGit, contentMerge);
+    RevCommit remoteHead = getRemoteHead();
+    assertFalse(remoteHead.getFooterLines("Reviewed-On").isEmpty());
+    assertFalse(remoteHead.getFooterLines("Reviewed-By").isEmpty());
+  }
+
+  protected void assertRebase(Git localGit, boolean contentMerge) throws IOException {
+    Repository repo = localGit.getRepository();
+    RevCommit localHead = getHead(repo);
+    RevCommit remoteHead = getRemoteHead();
+    assertNotEquals(localHead.getId(), remoteHead.getId());
+    assertEquals(1, remoteHead.getParentCount());
+    if (!contentMerge) {
+      assertEquals(getLatestDiff(repo), getLatestRemoteDiff());
+    }
+    assertEquals(localHead.getShortMessage(), remoteHead.getShortMessage());
+  }
+
+  private RevCommit getHead(Repository repo) throws IOException {
+    return getHead(repo, "HEAD");
+  }
+
+  protected RevCommit getRemoteHead() throws IOException {
+    Repository repo = repoManager.openRepository(project);
+    try {
+      return getHead(repo, "refs/heads/master");
+    } finally {
+      repo.close();
+    }
+  }
+
+  private RevCommit getHead(Repository repo, String name) throws IOException {
+    try {
+      RevWalk rw = new RevWalk(repo);
+      try {
+        return rw.parseCommit(repo.getRef(name).getObjectId());
+      } finally {
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  private String getLatestDiff(Repository repo) throws IOException {
+    ObjectId oldTreeId = repo.resolve("HEAD~1^{tree}");
+    ObjectId newTreeId = repo.resolve("HEAD^{tree}");
+    return getLatestDiff(repo, oldTreeId, newTreeId);
+  }
+
+  private String getLatestRemoteDiff() throws IOException {
+    Repository repo = repoManager.openRepository(project);
+    try {
+      RevWalk rw = new RevWalk(repo);
+      try {
+        ObjectId oldTreeId = repo.resolve("refs/heads/master~1^{tree}");
+        ObjectId newTreeId = repo.resolve("refs/heads/master^{tree}");
+        return getLatestDiff(repo, oldTreeId, newTreeId);
+      } finally {
+        rw.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  private String getLatestDiff(Repository repo, ObjectId oldTreeId,
+      ObjectId newTreeId) throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    DiffFormatter fmt = new DiffFormatter(out);
+    fmt.setRepository(repo);
+    fmt.format(oldTreeId, newTreeId);
+    fmt.flush();
+    return out.toString();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
new file mode 100644
index 0000000..770e554
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -0,0 +1,93 @@
+// 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.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.checkout;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public abstract class AbstractSubmitByMerge extends AbstractSubmit {
+
+  @Test
+  public void submitWithMerge() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "b.txt", "other content");
+    submit(change2.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(2, head.getParentCount());
+    assertEquals(oldHead, head.getParent(0));
+    assertEquals(change2.getCommitId(), head.getParent(1));
+  }
+
+  @Test
+  public void submitWithContentMerge() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "aaa\nbbb\nccc\n");
+    submit(change.getChangeId());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
+    submit(change2.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, change.getCommitId().getName());
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "a.txt", "bbb\nccc\n");
+    submit(change3.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(2, head.getParentCount());
+    assertEquals(oldHead, head.getParent(0));
+    assertEquals(change3.getCommitId(), head.getParent(1));
+  }
+
+  @Test
+  public void submitWithContentMerge_Conflict() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "other content");
+    submitWithConflict(change2.getChangeId());
+    assertEquals(oldHead, getRemoteHead());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AccountInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AccountInfo.java
new file mode 100644
index 0000000..ee94476
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AccountInfo.java
@@ -0,0 +1,21 @@
+// 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.acceptance.rest.change;
+
+public class AccountInfo {
+  public Integer _account_id;
+  public String name;
+  public String email;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
index dff94ce..af30705 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/BUCK
@@ -1,14 +1,40 @@
 include_defs('//gerrit-acceptance-tests/tests.defs')
 
 acceptance_tests(
-  srcs = glob(['*IT.java']),
+  srcs = ['ChangeMessagesIT.java'],
   deps = [
     ':util',
     '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
   ],
 )
 
+acceptance_tests(
+  srcs = ['SubmitByCherryPickIT.java', 'SubmitByFastForwardIT.java',
+          'SubmitByMergeAlwaysIT.java', 'SubmitByMergeIfNecessaryIT.java',
+          'SubmitByRebaseIfNecessaryIT.java'],
+  deps = [
+    ':submit',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
+  ],
+)
+
+java_library(
+  name = 'submit',
+  srcs = ['AbstractSubmit.java', 'AbstractSubmitByMerge.java'],
+  deps = [
+    ':util',
+    '//gerrit-acceptance-tests:lib',
+    '//gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git:util',
+  ],
+)
+
 java_library(
   name = 'util',
-  srcs = ['ChangeInfo.java', 'ChangeMessageInfo.java'],
+  srcs = ['AccountInfo.java', 'ChangeInfo.java', 'ChangeMessageInfo.java',
+          'GroupInfo.java', 'ProjectConfigInput.java', 'ReviewInput.java',
+          'SubmitInput.java', 'SuggestReviewerInfo.java'],
+  deps = [
+    '//lib:guava',
+    '//gerrit-reviewdb:server',
+  ],
 )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java
index 4c9325e..3921028 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeInfo.java
@@ -18,4 +18,5 @@
 
 public class ChangeInfo {
   List<ChangeMessageInfo> messages;
+  String status;
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/GroupInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/GroupInfo.java
new file mode 100644
index 0000000..2c0efff
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/GroupInfo.java
@@ -0,0 +1,20 @@
+// 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.acceptance.rest.change;
+
+public class GroupInfo {
+  public String id;
+  public String name;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ProjectConfigInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ProjectConfigInput.java
new file mode 100644
index 0000000..4d2e4b6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ProjectConfigInput.java
@@ -0,0 +1,23 @@
+// 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.acceptance.rest.change;
+
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+public class ProjectConfigInput {
+  public SubmitType submit_type;
+  public InheritableBoolean use_content_merge;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ReviewInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ReviewInput.java
new file mode 100644
index 0000000..a5371d2
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ReviewInput.java
@@ -0,0 +1,30 @@
+// 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.acceptance.rest.change;
+
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+
+public class ReviewInput {
+  Map<String, Integer> labels;
+
+  public static ReviewInput approve() {
+    ReviewInput in = new ReviewInput();
+    in.labels = Maps.newHashMap();
+    in.labels.put("Code-Review", 2);
+    return in;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
new file mode 100644
index 0000000..ccbbfb6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -0,0 +1,143 @@
+// 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.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.checkout;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByCherryPickIT extends AbstractSubmit {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.CHERRY_PICK;
+  }
+
+  @Test
+  public void submitWithCherryPickIfFastForwardPossible() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    assertCherryPick(git, false);
+    assertEquals(change.getCommit().getParent(0),
+        getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithCherryPick() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "b.txt", "other content");
+    submit(change2.getChangeId());
+    assertCherryPick(git, false);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithContentMerge() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "aaa\nbbb\nccc\n");
+    submit(change.getChangeId());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
+    submit(change2.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, change.getCommitId().getName());
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "a.txt", "bbb\nccc\n");
+    submit(change3.getChangeId());
+    assertCherryPick(git, true);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithContentMerge_Conflict() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "other content");
+    submitWithConflict(change2.getChangeId());
+    assertEquals(oldHead, getRemoteHead());
+  }
+
+  @Test
+  public void submitOutOfOrder() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    createChange(git, "Change 2", "b.txt", "other content");
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "c.txt", "different content");
+    submit(change3.getChangeId());
+    assertCherryPick(git, false);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitOutOfOrder_Conflict() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    createChange(git, "Change 2", "b.txt", "other content");
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "b.txt", "different content");
+    submitWithConflict(change3.getChangeId());
+    assertEquals(oldHead, getRemoteHead());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
new file mode 100644
index 0000000..9d56e18
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -0,0 +1,67 @@
+// 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.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.checkout;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByFastForwardIT extends AbstractSubmit {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.FAST_FORWARD_ONLY;
+  }
+
+  @Test
+  public void submitWithFastForward() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit oldHead = getRemoteHead();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(change.getCommitId(), head.getId());
+    assertEquals(oldHead, head.getParent(0));
+  }
+
+  @Test
+  public void submitFastForwardNotPossible_Conflict() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "b.txt", "other content");
+    submitWithConflict(change2.getChangeId());
+    assertEquals(oldHead, getRemoteHead());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
new file mode 100644
index 0000000..6c671eb
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeAlwaysIT.java
@@ -0,0 +1,50 @@
+// 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.acceptance.rest.change;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByMergeAlwaysIT extends AbstractSubmitByMerge {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.MERGE_ALWAYS;
+  }
+
+  @Test
+  public void submitWithMergeIfFastForwardPossible() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    RevCommit oldHead = getRemoteHead();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(2, head.getParentCount());
+    assertEquals(oldHead, head.getParent(0));
+    assertEquals(change.getCommitId(), head.getParent(1));
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
new file mode 100644
index 0000000..a5737a7
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -0,0 +1,35 @@
+package com.google.gerrit.acceptance.rest.change;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByMergeIfNecessaryIT extends AbstractSubmitByMerge {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.MERGE_IF_NECESSARY;
+  }
+
+  @Test
+  public void submitWithFastForward() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit oldHead = getRemoteHead();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(change.getCommitId(), head.getId());
+    assertEquals(oldHead, head.getParent(0));
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
new file mode 100644
index 0000000..07594a6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByRebaseIfNecessaryIT.java
@@ -0,0 +1,108 @@
+// 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.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.checkout;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+
+import com.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SubmitByRebaseIfNecessaryIT extends AbstractSubmit {
+
+  @Override
+  protected SubmitType getSubmitType() {
+    return SubmitType.REBASE_IF_NECESSARY;
+  }
+
+  @Test
+  public void submitWithFastForward() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit oldHead = getRemoteHead();
+    PushOneCommit.Result change = createChange(git);
+    submit(change.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(change.getCommitId(), head.getId());
+    assertEquals(oldHead, head.getParent(0));
+  }
+
+  @Test
+  public void submitWithRebase() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "b.txt", "other content");
+    submit(change2.getChangeId());
+    assertRebase(git, false);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithContentMerge() throws JSchException, IOException,
+      GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "aaa\nbbb\nccc\n");
+    submit(change.getChangeId());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "aaa\nbbb\nccc\nddd\n");
+    submit(change2.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, change.getCommitId().getName());
+    PushOneCommit.Result change3 =
+        createChange(git, "Change 3", "a.txt", "bbb\nccc\n");
+    submit(change3.getChangeId());
+    assertRebase(git, true);
+    assertEquals(oldHead, getRemoteHead().getParent(0));
+  }
+
+  @Test
+  public void submitWithContentMerge_Conflict() throws JSchException,
+      IOException, GitAPIException {
+    Git git = createProject();
+    setUseContentMerge();
+    RevCommit initialHead = getRemoteHead();
+    PushOneCommit.Result change =
+        createChange(git, "Change 1", "a.txt", "content");
+    submit(change.getChangeId());
+
+    RevCommit oldHead = getRemoteHead();
+    checkout(git, initialHead.getId().getName());
+    PushOneCommit.Result change2 =
+        createChange(git, "Change 2", "a.txt", "other content");
+    submitWithConflict(change2.getChangeId());
+    RevCommit head = getRemoteHead();
+    assertEquals(oldHead, head);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitInput.java
new file mode 100644
index 0000000..8e1b340
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitInput.java
@@ -0,0 +1,25 @@
+// 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.acceptance.rest.change;
+
+public class SubmitInput {
+  boolean wait_for_merge;
+
+  public static SubmitInput waitForMerge() {
+    SubmitInput in = new SubmitInput();
+    in.wait_for_merge = true;
+    return in;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewerInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewerInfo.java
new file mode 100644
index 0000000..212ee86
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewerInfo.java
@@ -0,0 +1,20 @@
+// 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.acceptance.rest.change;
+
+public class SuggestReviewerInfo {
+  public AccountInfo account;
+  public GroupInfo group;
+}
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
new file mode 100644
index 0000000..1ed4d61
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -0,0 +1,154 @@
+// 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.acceptance.rest.change;
+
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.GerritConfigs;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.git.PushOneCommit;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+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;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SuggestReviewersIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private TestAccount admin;
+  private RestSession session;
+  private Git git;
+  private ReviewDb db;
+  private Project.NameKey project;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.admin();
+    session = new RestSession(server, admin);
+
+    group("users1");
+    group("users2");
+    group("users3");
+
+    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");
+
+    initSsh(admin);
+    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);
+    List<SuggestReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    assertEquals(reviewers.size(), 0);
+  }
+
+  @Test
+  @GerritConfigs(
+      {@GerritConfig(name = "suggest.accounts", value = "true"),
+       @GerritConfig(name = "suggest.from", value = "1"),
+       @GerritConfig(name = "accounts.visibility", value = "NONE"),
+      })
+  public void suggestReviewersNoResult2() throws GitAPIException, IOException,
+      Exception {
+    String changeId = createChange(admin);
+    List<SuggestReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    assertEquals(reviewers.size(), 0);
+  }
+
+  @Test
+  @GerritConfig(name = "suggest.from", value = "2")
+  public void suggestReviewersNoResult3() throws GitAPIException, IOException,
+      Exception {
+    String changeId = createChange(admin);
+    List<SuggestReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    assertEquals(reviewers.size(), 0);
+  }
+
+  @Test
+  public void suggestReviewersChange() throws GitAPIException,
+      IOException, Exception {
+    String changeId = createChange(admin);
+    List<SuggestReviewerInfo> reviewers = suggestReviewers(changeId, "u", 6);
+    assertEquals(reviewers.size(), 6);
+    reviewers = suggestReviewers(changeId, "u", 5);
+    assertEquals(reviewers.size(), 5);
+    reviewers = suggestReviewers(changeId, "users3", 10);
+    assertEquals(reviewers.size(), 1);
+  }
+
+  private List<SuggestReviewerInfo> suggestReviewers(String changeId,
+      String query, int n)
+      throws IOException {
+    return new Gson().fromJson(
+        session.get("/changes/"
+            + changeId
+            + "/suggest_reviewers?q="
+            + query
+            + "&n="
+            + n)
+        .getReader(),
+        new TypeToken<List<SuggestReviewerInfo>>() {}
+        .getType());
+  }
+
+  private void group(String name) throws IOException {
+    session.put("/groups/" + name, new Object()).consume();
+  }
+
+  private String createChange(TestAccount account) throws GitAPIException,
+      IOException {
+    PushOneCommit push = new PushOneCommit(db, account.getIdent());
+    return push.to(git, "refs/for/master").getChangeId();
+  }
+}
\ No newline at end of file
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 ad415d4..05425a5 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
@@ -54,12 +54,8 @@
     return "/c/" + c + "/";
   }
 
-  public static String toChange2(final Change.Id c) {
-    return "/c2/" + c + "/";
-  }
-
-  public static String toChange2(Change.Id c, String p) {
-    return "/c2/" + c + "/" + p;
+  public static String toChange(Change.Id c, String p) {
+    return "/c/" + c + "/" + p;
   }
 
   public static String toChange(final PatchSet.Id ps) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
index f85e333..c4c388f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AuthType;
@@ -51,6 +52,7 @@
   protected String anonymousCowardName;
   protected int suggestFrom;
   protected int changeUpdateDelay;
+  protected AccountGeneralPreferences.ChangeScreen changeScreen;
 
   public String getLoginUrl() {
     return loginUrl;
@@ -270,4 +272,12 @@
   public void setChangeUpdateDelay(int seconds) {
     changeUpdateDelay = seconds;
   }
+
+  public AccountGeneralPreferences.ChangeScreen getChangeScreen() {
+    return changeScreen;
+  }
+
+  public void setChangeScreen(AccountGeneralPreferences.ChangeScreen ui) {
+    this.changeScreen = ui;
+  }
 }
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 685c4b8..3c4e334 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
@@ -81,6 +81,7 @@
 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.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -93,8 +94,6 @@
 import com.google.gwtorm.client.KeyUtil;
 
 public class Dispatcher {
-  private static boolean useChangeScreen2;
-
   public static String toPatchSideBySide(final Patch.Key id) {
     return toPatch("", null, id);
   }
@@ -203,9 +202,6 @@
     } else if (matchPrefix("/c/", token)) {
       change(token);
 
-    } else if (matchPrefix("/c2/", token)) {
-      change2(token);
-
     } else if (matchExact(MINE, token)) {
       Gerrit.display(token, mine(token));
 
@@ -480,7 +476,7 @@
 
     if (rest.isEmpty()) {
       Gerrit.display(token, panel== null
-          ? (useChangeScreen2
+          ? (isChangeScreen2()
               ? new ChangeScreen2(id, null, false)
               : new ChangeScreen(id))
           : new NotFoundScreen());
@@ -513,7 +509,7 @@
       patch(token, base, p, 0, null, null, panel);
     } else {
       if (panel == null) {
-        Gerrit.display(token, useChangeScreen2
+        Gerrit.display(token, isChangeScreen2()
             ? new ChangeScreen2(id, String.valueOf(ps.get()), false)
             : new ChangeScreen(id));
       } else if ("publish".equals(panel)) {
@@ -524,19 +520,17 @@
     }
   }
 
-  private static void change2(final String token) {
-    String rest = skip(token);
-    Change.Id id;
-    int s = rest.indexOf('/');
-    if (0 <= s) {
-      id = Change.Id.parse(rest.substring(0, s));
-      rest = rest.substring(s + 1);
-    } else {
-      id = Change.Id.parse(rest);
-      rest = "";
+  private static boolean isChangeScreen2() {
+    AccountGeneralPreferences.ChangeScreen ui = null;
+    if (Gerrit.isSignedIn()) {
+      ui = Gerrit.getUserAccount()
+          .getGeneralPreferences()
+          .getChangeScreen();
     }
-    useChangeScreen2 = true;
-    Gerrit.display(token, new ChangeScreen2(id, rest, false));
+    if (ui == null) {
+      ui = Gerrit.getConfig().getChangeScreen();
+    }
+    return ui == AccountGeneralPreferences.ChangeScreen.CHANGE_SCREEN2;
   }
 
   private static void publish(final PatchSet.Id ps) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index fb72085..717bf8c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -48,6 +48,8 @@
 
   String inactiveAccountBody();
 
+  String labelNotApplicable();
+
   String menuAll();
   String menuAllOpen();
   String menuAllMerged();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 6f3dca5..4c4fcf7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -31,6 +31,8 @@
 
 inactiveAccountBody = This user is currently inactive.
 
+labelNotApplicable = Label not applicable
+
 menuAll = All
 menuAllOpen = Open
 menuAllMerged = Merged
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 2fef7ee..b285c0b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -141,6 +141,7 @@
   String infoTable();
   String inputFieldTypeHint();
   String labelList();
+  String labelNotApplicable();
   String leftMostCell();
   String lineHeader();
   String lineNumber();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 28a96149..e0e2d22 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -26,6 +26,7 @@
   String accountId();
 
   String commentVisibilityLabel();
+  String changeScreenLabel();
   String diffViewLabel();
   String maximumPageSizeFieldLabel();
   String dateFormatLabel();
@@ -38,6 +39,9 @@
   String buttonSaveChanges();
   String showRelativeDateInChangeTable();
 
+  String changeScreenOldUi();
+  String changeScreenNewUi();
+
   String tabAccountSummary();
   String tabPreferences();
   String tabWatchedProjects();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index ab7e5a9..fde1c2e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -12,12 +12,16 @@
 showUsernameInReviewCategory = Display Person Name In Review Category
 maximumPageSizeFieldLabel = Maximum Page Size:
 commentVisibilityLabel = Comment Visibility:
-diffViewLabel = Diff View (ChangeScreen2 only):
+changeScreenLabel = Change View:
+diffViewLabel = Diff View (Change Screen 2):
 dateFormatLabel = Date/Time Format:
 contextWholeFile = Whole File
 buttonSaveChanges = Save Changes
 showRelativeDateInChangeTable = Show Relative Dates in Changes Table
 
+changeScreenOldUi = Old Screen
+changeScreenNewUi = Change Screen 2
+
 tabAccountSummary = Profile
 tabPreferences = Preferences
 tabWatchedProjects = Watched Projects
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
index 2014f19..e55be79 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.java
@@ -21,6 +21,7 @@
 public interface AccountMessages extends Messages {
   String lines(short cnt);
   String rowsPerPage(short cnt);
+  String changeScreenServerDefault(String d);
   String enterIAGREE(String iagree);
   String contactOnFile(Date lastDate);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
index d013911..313893e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
@@ -1,5 +1,7 @@
 lines = {0} lines
 rowsPerPage = {0} rows per page
 
+changeScreenServerDefault = Server Default ({0}) 
+
 enterIAGREE = (enter {0} in the box to the left)
 contactOnFile = Contact information last updated on {0,date,medium} at {0,time,short}.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index dbc8480..115a46f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -48,6 +48,7 @@
   private ListBox dateFormat;
   private ListBox timeFormat;
   private ListBox commentVisibilityStrategy;
+  private ListBox changeScreen;
   private ListBox diffView;
   private Button save;
 
@@ -68,30 +69,36 @@
     commentVisibilityStrategy = new ListBox();
     commentVisibilityStrategy.addItem(
         com.google.gerrit.client.changes.Util.C.messageCollapseAll(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.COLLAPSE_ALL.name()
-    );
+        AccountGeneralPreferences.CommentVisibilityStrategy.COLLAPSE_ALL.name());
     commentVisibilityStrategy.addItem(
         com.google.gerrit.client.changes.Util.C.messageExpandMostRecent(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT.name()
-    );
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_MOST_RECENT.name());
     commentVisibilityStrategy.addItem(
         com.google.gerrit.client.changes.Util.C.messageExpandRecent(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT.name()
-    );
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT.name());
     commentVisibilityStrategy.addItem(
         com.google.gerrit.client.changes.Util.C.messageExpandAll(),
-        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_ALL.name()
-    );
+        AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_ALL.name());
+
+    changeScreen = new ListBox();
+    changeScreen.addItem(
+        Util.M.changeScreenServerDefault(
+            getLabel(Gerrit.getConfig().getChangeScreen())),
+        "");
+    changeScreen.addItem(
+        Util.C.changeScreenOldUi(),
+        AccountGeneralPreferences.ChangeScreen.OLD_UI.name());
+    changeScreen.addItem(
+        Util.C.changeScreenNewUi(),
+        AccountGeneralPreferences.ChangeScreen.CHANGE_SCREEN2.name());
 
     diffView = new ListBox();
     diffView.addItem(
         com.google.gerrit.client.changes.Util.C.sideBySide(),
-        AccountGeneralPreferences.DiffView.SIDE_BY_SIDE.name()
-    );
+        AccountGeneralPreferences.DiffView.SIDE_BY_SIDE.name());
     diffView.addItem(
         com.google.gerrit.client.changes.Util.C.unifiedDiff(),
-        AccountGeneralPreferences.DiffView.UNIFIED_DIFF.name()
-    );
+        AccountGeneralPreferences.DiffView.UNIFIED_DIFF.name());
 
     Date now = new Date();
     dateFormat = new ListBox();
@@ -129,7 +136,7 @@
 
     relativeDateInChangeTable = new CheckBox(Util.C.showRelativeDateInChangeTable());
 
-    final Grid formGrid = new Grid(10, 2);
+    final Grid formGrid = new Grid(11, 2);
 
     int row = 0;
     formGrid.setText(row, labelIdx, "");
@@ -168,6 +175,10 @@
     formGrid.setWidget(row, fieldIdx, commentVisibilityStrategy);
     row++;
 
+    formGrid.setText(row, labelIdx, Util.C.changeScreenLabel());
+    formGrid.setWidget(row, fieldIdx, changeScreen);
+    row++;
+
     formGrid.setText(row, labelIdx, Util.C.diffViewLabel());
     formGrid.setWidget(row, fieldIdx, diffView);
     row++;
@@ -195,6 +206,7 @@
     e.listenTo(timeFormat);
     e.listenTo(relativeDateInChangeTable);
     e.listenTo(commentVisibilityStrategy);
+    e.listenTo(changeScreen);
     e.listenTo(diffView);
   }
 
@@ -219,6 +231,7 @@
     timeFormat.setEnabled(on);
     relativeDateInChangeTable.setEnabled(on);
     commentVisibilityStrategy.setEnabled(on);
+    changeScreen.setEnabled(on);
     diffView.setEnabled(on);
   }
 
@@ -237,6 +250,9 @@
     setListBox(commentVisibilityStrategy,
         AccountGeneralPreferences.CommentVisibilityStrategy.EXPAND_RECENT,
         p.getCommentVisibilityStrategy());
+    setListBox(changeScreen,
+        null,
+        p.getChangeScreen());
     setListBox(diffView,
         AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
         p.getDiffView());
@@ -249,7 +265,8 @@
 
   private <T extends Enum<?>> void setListBox(final ListBox f,
       final T defaultValue, final T currentValue) {
-    setListBox(f, defaultValue.name(), //
+    setListBox(f,
+        defaultValue != null ? defaultValue.name() : "",
         currentValue != null ? currentValue.name() : "");
   }
 
@@ -280,6 +297,9 @@
     final int idx = f.getSelectedIndex();
     if (0 <= idx) {
       String v = f.getValue(idx);
+      if ("".equals(v)) {
+        return defaultValue;
+      }
       for (T t : all) {
         if (t.name().equals(v)) {
           return t;
@@ -310,6 +330,9 @@
     p.setDiffView(getListBox(diffView,
         AccountGeneralPreferences.DiffView.SIDE_BY_SIDE,
         AccountGeneralPreferences.DiffView.values()));
+    p.setChangeScreen(getListBox(changeScreen,
+        null,
+        AccountGeneralPreferences.ChangeScreen.values()));
 
     enable(false);
     save.setEnabled(false);
@@ -330,4 +353,18 @@
       }
     });
   }
+
+  private static String getLabel(AccountGeneralPreferences.ChangeScreen ui) {
+    if (ui == null) {
+      return "";
+    }
+    switch (ui) {
+      case OLD_UI:
+        return Util.C.changeScreenOldUi();
+      case CHANGE_SCREEN2:
+        return Util.C.changeScreenNewUi();
+      default:
+        return ui.name();
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
index 4faed50..77724f8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
@@ -38,7 +38,7 @@
             Window.alert(str.asString());
           }
         }
-        Gerrit.display(PageLinks.toChange2(id));
+        Gerrit.display(PageLinks.toChange(id));
       }
     };
     if ("PUT".equalsIgnoreCase(action.method())) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
index d46edd3..60fbee4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AbandonAction.java
@@ -34,7 +34,7 @@
     ChangeApi.abandon(id.get(), message, new GerritCallback<ChangeInfo>() {
       @Override
       public void onSuccess(ChangeInfo result) {
-        Gerrit.display(PageLinks.toChange2(id));
+        Gerrit.display(PageLinks.toChange(id));
         hide();
       }
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
index 09aef22..c6940de 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
@@ -120,7 +120,7 @@
   @UiField AnchorElement permalink;
 
   @UiField Element reviewersText;
-  @UiField Element ccText;
+  @UiField Reviewers reviewers;
   @UiField Element changeIdText;
   @UiField Element ownerText;
   @UiField Element statusText;
@@ -208,6 +208,7 @@
     Resources.I.style().ensureInjected();
     star.setVisible(Gerrit.isSignedIn());
     labels.init(style, statusText);
+    reviewers.init(style);
 
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
@@ -241,6 +242,12 @@
           star.setValue(!star.getValue(), true);
         }
       });
+      keysAction.add(new KeyCommand(0, 'c', Util.C.keyAddReviewers()) {
+        @Override
+        public void onKeyPress(KeyPressEvent event) {
+          reviewers.onOpenForm();
+        }
+      });
     }
   }
 
@@ -654,8 +661,9 @@
     }
     r.remove(info.owner()._account_id());
     cc.remove(info.owner()._account_id());
-    reviewersText.setInnerSafeHtml(labels.formatUserList(r.values()));
-    ccText.setInnerSafeHtml(labels.formatUserList(cc.values()));
+    reviewersText.setInnerSafeHtml(Labels.formatUserList(style, r.values()));
+    reviewers.set(info.legacy_id());
+    reviewers.setReviewers(Labels.formatUserList(style, cc.values()));
   }
 
   private void renderOwner(ChangeInfo info) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
index 6cf7b7b..2c47c4d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.ui.xml
@@ -281,7 +281,9 @@
             </tr>
             <tr>
               <th><ui:msg>CC</ui:msg></th>
-              <td ui:field='ccText'/>
+              <td>
+                <c:Reviewers ui:field='reviewers'/>
+              </td>
             </tr>
             <tr>
               <th><ui:msg>Project</ui:msg></th>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
index 2c0db0b..0dc155e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/CherryPickAction.java
@@ -51,7 +51,7 @@
               public void onSuccess(ChangeInfo result) {
                 sent = true;
                 hide();
-                Gerrit.display(PageLinks.toChange2(result.legacy_id()));
+                Gerrit.display(PageLinks.toChange(result.legacy_id()));
               }
 
               @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
index 5ddd21b..cd1f304 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditMessageBox.java
@@ -80,7 +80,7 @@
         new GerritCallback<JavaScriptObject>() {
           @Override
           public void onSuccess(JavaScriptObject msg) {
-            Gerrit.display(PageLinks.toChange2(changeId));
+            Gerrit.display(PageLinks.toChange(changeId));
             hide();
           };
         });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
index f3ce7fc..6316c64 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Labels.java
@@ -123,7 +123,7 @@
         html.setStyleName(style.label_reject());
       }
       html.append(val).append(" ");
-      html.append(formatUserList(m.get(v)));
+      html.append(formatUserList(style, m.get(v)));
       html.closeSpan();
     }
     return html.toBlockWidget();
@@ -167,7 +167,8 @@
     }
   }
 
-  SafeHtml formatUserList(Collection<? extends AccountInfo> in) {
+  static SafeHtml formatUserList(ChangeScreen2.Style style,
+      Collection<? extends AccountInfo> in) {
     List<AccountInfo> users = new ArrayList<AccountInfo>(in);
     Collections.sort(users, new Comparator<AccountInfo>() {
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
index 7836de5..ddb7d56 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/QuickApprove.java
@@ -112,7 +112,7 @@
       .post(input, new GerritCallback<ReviewInput>() {
         @Override
         public void onSuccess(ReviewInput result) {
-          Gerrit.display(PageLinks.toChange2(changeId));
+          Gerrit.display(PageLinks.toChange(changeId));
         }
       });
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
index 0dd6072..2b1c250 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RebaseAction.java
@@ -26,7 +26,7 @@
     ChangeApi.rebase(id.get(), revision,
       new GerritCallback<ChangeInfo>() {
         public void onSuccess(ChangeInfo result) {
-          Gerrit.display(PageLinks.toChange2(id));
+          Gerrit.display(PageLinks.toChange(id));
         }
       });
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
index cddf1bf..f8a7cc7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -174,7 +174,7 @@
   private String url(ChangeAndCommit c) {
     if (c.has_change_number() && c.has_revision_number()) {
       PatchSet.Id id = c.patch_set_id();
-      return "#" + PageLinks.toChange2(
+      return "#" + PageLinks.toChange(
           id.getParentKey(),
           String.valueOf(id.get()));
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java
index bd0c0e2..7b88b2b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reload.java
@@ -47,7 +47,7 @@
   }
 
   void reload() {
-    Gerrit.display(PageLinks.toChange2(changeId));
+    Gerrit.display(PageLinks.toChange(changeId));
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index 01110de..f17c01f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -144,7 +144,7 @@
       .post(in, new GerritCallback<ReviewInput>() {
         @Override
         public void onSuccess(ReviewInput result) {
-          Gerrit.display(PageLinks.toChange2(
+          Gerrit.display(PageLinks.toChange(
               psId.getParentKey(),
               String.valueOf(psId.get())));
         }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestReviewerSuggestOracle.java
new file mode 100644
index 0000000..7575869
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestReviewerSuggestOracle.java
@@ -0,0 +1,89 @@
+// 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.client.change;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.admin.Util;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.groups.GroupBaseInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
+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.user.client.ui.SuggestOracle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** REST API based suggestion Oracle for reviewers. */
+public class RestReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle {
+
+  private Change.Id changeId;
+
+  @Override
+  protected void _onRequestSuggestions(final Request req, final Callback callback) {
+    ChangeApi.suggestReviewers(changeId.get(), req.getQuery(),
+        req.getLimit()).get(new GerritCallback<JsArray<SuggestReviewerInfo>>() {
+          @Override
+          public void onSuccess(JsArray<SuggestReviewerInfo> result) {
+            final List<RestReviewerSuggestion> r =
+                new ArrayList<RestReviewerSuggestion>(result.length());
+            for (final SuggestReviewerInfo reviewer : Natives.asList(result)) {
+              r.add(new RestReviewerSuggestion(reviewer));
+            }
+            callback.onSuggestionsReady(req, new Response(r));
+          }
+        });
+  }
+
+  public void setChange(Change.Id changeId) {
+    this.changeId = changeId;
+  }
+
+  private static class RestReviewerSuggestion implements SuggestOracle.Suggestion {
+    private final SuggestReviewerInfo reviewer;
+
+    RestReviewerSuggestion(final SuggestReviewerInfo reviewer) {
+      this.reviewer = reviewer;
+    }
+
+    public String getDisplayString() {
+      if (reviewer.account() != null) {
+        return FormatUtil.nameEmail(reviewer.account());
+      }
+      return reviewer.group().name()
+          + " ("
+          + Util.C.suggestedGroupLabel()
+          + ")";
+    }
+
+    public String getReplacementString() {
+      if (reviewer.account() != null) {
+        return FormatUtil.nameEmail(reviewer.account());
+      }
+      return reviewer.group().name();
+    }
+  }
+
+  public static class SuggestReviewerInfo extends JavaScriptObject {
+    public final native AccountInfo account() /*-{ return this.account; }-*/;
+    public final native GroupBaseInfo group() /*-{ return this.group; }-*/;
+    protected SuggestReviewerInfo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
index 82f9b0c..215f39d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RestoreAction.java
@@ -34,7 +34,7 @@
     ChangeApi.restore(id.get(), message, new GerritCallback<ChangeInfo>() {
       @Override
       public void onSuccess(ChangeInfo result) {
-        Gerrit.display(PageLinks.toChange2(id));
+        Gerrit.display(PageLinks.toChange(id));
         hide();
       }
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
index 3190d79..5f9f076 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
@@ -46,7 +46,7 @@
               public void onSuccess(ChangeInfo result) {
                 sent = true;
                 hide();
-                Gerrit.display(PageLinks.toChange2(result.legacy_id()));
+                Gerrit.display(PageLinks.toChange(result.legacy_id()));
               }
 
               @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
new file mode 100644
index 0000000..b4e1f55
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -0,0 +1,225 @@
+// 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.client.change;
+
+import com.google.gerrit.client.ConfirmationCallback;
+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.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.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.event.logical.shared.SelectionHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.rpc.StatusCodeException;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Add reviewers. */
+class Reviewers extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, Reviewers> {}
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField Button openForm;
+  @UiField Element reviewers;
+  @UiField Element form;
+  @UiField Element error;
+  @UiField(provided = true)
+  SuggestBox suggestBox;
+
+  private RestReviewerSuggestOracle reviewerSuggestOracle;
+  private HintTextBox nameTxtBox;
+  private Change.Id changeId;
+  private ChangeScreen2.Style style;
+  private boolean submitOnSelection;
+
+  Reviewers() {
+    reviewerSuggestOracle = new RestReviewerSuggestOracle();
+    nameTxtBox = new HintTextBox();
+    suggestBox = new SuggestBox(reviewerSuggestOracle, nameTxtBox);
+    initWidget(uiBinder.createAndBindUi(this));
+
+    nameTxtBox.setVisibleLength(55);
+    nameTxtBox.setHintText(Util.C.approvalTableAddReviewerHint());
+    nameTxtBox.addKeyDownHandler(new KeyDownHandler() {
+      @Override
+      public void onKeyDown(KeyDownEvent e) {
+        submitOnSelection = false;
+
+        if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
+          onCancel(null);
+        } else if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+          if (((DefaultSuggestionDisplay) suggestBox.getSuggestionDisplay())
+              .isSuggestionListShowing()) {
+            submitOnSelection = true;
+          } else {
+            onAdd(null);
+          }
+        }
+      }
+    });
+    suggestBox.addSelectionHandler(new SelectionHandler<Suggestion>() {
+      @Override
+      public void onSelection(SelectionEvent<Suggestion> event) {
+        nameTxtBox.setFocus(true);
+        if (submitOnSelection) {
+          onAdd(null);
+        }
+      }
+    });
+  }
+
+  void set(Change.Id changeId) {
+    this.changeId = changeId;
+    reviewerSuggestOracle.setChange(changeId);
+  }
+
+  void init(ChangeScreen2.Style style) {
+    this.style = style;
+    openForm.setVisible(Gerrit.isSignedIn());
+  }
+
+  void setReviewers(SafeHtml formatUserList) {
+    reviewers.setInnerSafeHtml(formatUserList);
+  }
+
+  @UiHandler("openForm")
+  void onOpenForm(ClickEvent e) {
+    onOpenForm();
+  }
+
+  void onOpenForm() {
+    UIObject.setVisible(form, true);
+    UIObject.setVisible(error, false);
+    openForm.setVisible(false);
+    suggestBox.setFocus(true);
+  }
+
+  @UiHandler("add")
+  void onAdd(ClickEvent e) {
+    String reviewer = suggestBox.getText();
+    if (!reviewer.isEmpty()) {
+      addReviewer(reviewer, false);
+    }
+  }
+
+  @UiHandler("cancel")
+  void onCancel(ClickEvent e) {
+    openForm.setVisible(true);
+    UIObject.setVisible(form, false);
+    suggestBox.setFocus(false);
+  }
+
+  private void addReviewer(final String reviewer, boolean confirmed) {
+    ChangeApi.reviewers(changeId.get()).post(
+        PostInput.create(reviewer, confirmed),
+        new GerritCallback<PostResult>() {
+          public void onSuccess(PostResult result) {
+            nameTxtBox.setEnabled(true);
+
+            if (result.confirm()) {
+              askForConfirmation(result.error());
+            } else if (result.error() != null) {
+              UIObject.setVisible(error, true);
+              error.setInnerText(result.error());
+            } else {
+              UIObject.setVisible(error, false);
+              error.setInnerText("");
+              nameTxtBox.setText("");
+
+              if (result.reviewers() != null
+                  && result.reviewers().length() > 0) {
+                updateReviewerList();
+              }
+            }
+          }
+
+          private void askForConfirmation(String text) {
+            new ConfirmationDialog(
+                Util.C.approvalTableAddManyReviewersConfirmationDialogTitle(),
+                new SafeHtmlBuilder().append(text),
+                new ConfirmationCallback() {
+                  @Override
+                  public void onOk() {
+                    addReviewer(reviewer, true);
+                  }
+                }).center();
+          }
+
+          @Override
+          public void onFailure(Throwable err) {
+            UIObject.setVisible(error, true);
+            error.setInnerText(err instanceof StatusCodeException
+                ? ((StatusCodeException) err).getEncodedResponse()
+                : err.getMessage());
+            nameTxtBox.setEnabled(true);
+          }
+        });
+  }
+
+  private void updateReviewerList() {
+    ChangeApi.detail(changeId.get(),
+        new GerritCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo result) {
+            display(result);
+          }
+        });
+  }
+
+  private void display(ChangeInfo info) {
+    Map<Integer, AccountInfo> r = new HashMap<Integer, AccountInfo>();
+    Map<Integer, AccountInfo> cc = new HashMap<Integer, AccountInfo>();
+    for (LabelInfo label : Natives.asList(info.all_labels().values())) {
+      if (label.all() != null) {
+        for (ApprovalInfo ai : Natives.asList(label.all())) {
+          (ai.value() != 0 ? r : cc).put(ai._account_id(), ai);
+        }
+      }
+    }
+    for (Integer i : r.keySet()) {
+      cc.remove(i);
+    }
+    cc.remove(info.owner()._account_id());
+    setReviewers(Labels.formatUserList(style, cc.values()));
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
new file mode 100644
index 0000000..568e950
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.ui.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+    xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+  <ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
+  <ui:style>
+    .openAdd {
+      cursor: pointer;
+      float: right;
+      padding: 0;
+      margin: 0;
+      border: 0;
+      background-color: transparent;
+    }
+
+    .suggestBox {
+      margin-bottom: 2px;
+    }
+
+    .error {
+      color: #D33D3D;
+      font-weight: bold;
+    }
+
+    .cancel {
+      float: right;
+    }
+  </ui:style>
+  <g:HTMLPanel>
+    <div>
+      <span ui:field='reviewers'/>
+      <g:Button ui:field='openForm'
+         title='Add reviewers to this change'
+         styleName='{style.openAdd}'
+         visible='false'>
+       <ui:attribute name='title'/>
+       <div>[+]</div>
+      </g:Button>
+    </div>
+    <div ui:field='form' style='display: none' aria-hidden='true'>
+      <g:SuggestBox ui:field='suggestBox' styleName='{style.suggestBox}'/>
+      <div ui:field='error'
+           class='{style.error}'
+           style='display: none' aria-hidden='true'/>
+      <div>
+        <g:Button ui:field='add' styleName='{res.style.button}'>
+          <div>Add</div>
+        </g:Button>
+        <g:Button ui:field='cancel'
+            styleName='{res.style.button}'
+            addStyleNames='{style.cancel}'>
+          <div>Cancel</div>
+        </g:Button>
+      </div>
+    </div>
+   </g:HTMLPanel>
+  </ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java
index c0b2112..4ebf3819 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevisionsBox.java
@@ -203,7 +203,7 @@
   }
 
   private String url(RevisionInfo r) {
-    return PageLinks.toChange2(
+    return PageLinks.toChange(
         changeId,
         String.valueOf(r._number()));
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
index 3008830..8a3aae5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/SubmitAction.java
@@ -40,7 +40,7 @@
         }
 
         private void redisplay() {
-          Gerrit.display(PageLinks.toChange2(id));
+          Gerrit.display(PageLinks.toChange(id));
         }
       });
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
index fff109a..4f98bdb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Topic.java
@@ -134,7 +134,7 @@
         new GerritCallback<String>() {
           @Override
           public void onSuccess(String result) {
-            Gerrit.display(PageLinks.toChange2(
+            Gerrit.display(PageLinks.toChange(
                 psId.getParentKey(),
                 String.valueOf(psId.get())));
           }
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 93c9505..350f97f 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
@@ -254,8 +254,8 @@
     }
   }
 
-  private static class PostInput extends JavaScriptObject {
-    static PostInput create(String reviewer, boolean confirmed) {
+  public static class PostInput extends JavaScriptObject {
+    public static PostInput create(String reviewer, boolean confirmed) {
       PostInput input = createObject().cast();
       input.init(reviewer, confirmed);
       return input;
@@ -272,7 +272,7 @@
     }
   }
 
-  private static class ReviewerInfo extends AccountInfo {
+  public static class ReviewerInfo extends AccountInfo {
     final Set<String> approvals() {
       return Natives.keys(_approvals());
     }
@@ -283,10 +283,10 @@
     }
   }
 
-  private static class PostResult extends JavaScriptObject {
-    final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/;
-    final native boolean confirm() /*-{ return this.confirm || false; }-*/;
-    final native String error() /*-{ return this.error; }-*/;
+  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/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index c292154..9d76104 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -86,6 +86,12 @@
     return change(id).view("reviewers");
   }
 
+  public static RestApi suggestReviewers(int id, String q, int n) {
+    return change(id).view("suggest_reviewers")
+        .addParameter("q", q)
+        .addParameter("n", n);
+  }
+
   public static RestApi reviewer(int id, int reviewer) {
     return change(id).view("reviewers").id(reviewer);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 197b466..81ab944 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -61,6 +61,7 @@
   String keyPublishComments();
   String keyEditTopic();
   String keyEditMessage();
+  String keyAddReviewers();
 
   String patchTableColumnName();
   String patchTableColumnComments();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index f51cff0..86623dc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -41,6 +41,7 @@
 keyPublishComments = Review and publish comments
 keyEditTopic = Edit change topic
 keyEditMessage = Edit commit message
+keyAddReviewers = Add reviewers
 
 
 patchTableColumnName = File Path
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
index f993ad9..cedbd4b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -230,7 +230,8 @@
 
       LabelInfo label = c.label(name);
       if (label == null) {
-        table.clearCell(row, col);
+        fmt.getElement(row, col).setTitle(Gerrit.C.labelNotApplicable());
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().labelNotApplicable());
         continue;
       }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index c46762c..e5c390c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -73,7 +73,7 @@
     this.path = path;
 
     SafeHtml.setInnerHTML(filePath, formatPath(path));
-    up.setTargetHistoryToken(PageLinks.toChange2(
+    up.setTargetHistoryToken(PageLinks.toChange(
         patchSetId.getParentKey(),
         String.valueOf(patchSetId.get())));
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
index 951534b..1094dae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide2.java
@@ -1096,7 +1096,7 @@
       public void run() {
         String rev = String.valueOf(revision.get());
         Gerrit.display(
-          PageLinks.toChange2(changeId, rev),
+          PageLinks.toChange(changeId, rev),
           new ChangeScreen2(changeId, rev, openReplyBox));
       }
     };
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
index f40c4c1..7071e7f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand2.java
@@ -31,7 +31,7 @@
 
   @Override
   public void onKeyPress(final KeyPressEvent event) {
-    Gerrit.display(PageLinks.toChange2(
+    Gerrit.display(PageLinks.toChange(
         revision.getParentKey(),
         String.valueOf(revision.get())));
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 8087b68..908c07f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -595,6 +595,9 @@
 .changeTable .dataCell.singleLine {
   white-space: nowrap;
 }
+.changeTable .dataCell.labelNotApplicable {
+ background: #F5F5F5;
+}
 .changeTable .iconHeader {
   border-top: 1px solid backgroundColor;
   border-bottom: 1px solid backgroundColor;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java
new file mode 100644
index 0000000..4811e59
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java
@@ -0,0 +1,31 @@
+// 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.client.groups;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.URL;
+
+public class GroupBaseInfo extends JavaScriptObject {
+  public final AccountGroup.UUID getGroupUUID() {
+    return new AccountGroup.UUID(URL.decodeQueryString(id()));
+  }
+
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String name() /*-{ return this.name; }-*/;
+
+  protected GroupBaseInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
index f529990..f1e4e87 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
@@ -20,17 +20,11 @@
 import com.google.gwt.core.client.JsArray;
 import com.google.gwt.http.client.URL;
 
-public class GroupInfo extends JavaScriptObject {
+public class GroupInfo extends GroupBaseInfo {
   public final AccountGroup.Id getGroupId() {
     return new AccountGroup.Id(group_id());
   }
 
-  public final AccountGroup.UUID getGroupUUID() {
-    return new AccountGroup.UUID(URL.decodeQueryString(id()));
-  }
-
-  public final native String id() /*-{ return this.id; }-*/;
-  public final native String name() /*-{ return this.name; }-*/;
   public final native GroupOptionsInfo options() /*-{ return this.options; }-*/;
   public final native String description() /*-{ return this.description; }-*/;
   public final native String url() /*-{ return this.url; }-*/;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index 677a615..2980159 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.GitwebConfig;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
@@ -124,6 +125,9 @@
     config.setSuggestFrom(cfg.getInt("suggest", "from", 0));
     config.setChangeUpdateDelay((int) ConfigUtil.getTimeUnit(
         cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS));
+    config.setChangeScreen(cfg.getEnum(
+        "gerrit", null, "changeScreen",
+        AccountGeneralPreferences.ChangeScreen.OLD_UI));
 
     config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
     if (config.getReportBugUrl() == null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
index 63c22ce..0f088c9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -39,6 +40,7 @@
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
   private final GitReferenceUpdated gitRefUpdated;
+  private final ChangeIndexer indexer;
 
   private final PatchSet.Id patchSetId;
 
@@ -47,11 +49,13 @@
       final ChangeControl.Factory changeControlFactory,
       final GitRepositoryManager gitManager,
       final GitReferenceUpdated gitRefUpdated,
+      final ChangeIndexer indexer,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.gitManager = gitManager;
     this.gitRefUpdated = gitRefUpdated;
+    this.indexer = indexer;
 
     this.patchSetId = patchSetId;
   }
@@ -65,7 +69,8 @@
       throw new NoSuchChangeException(changeId);
     }
 
-    ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db);
+    ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db,
+        indexer);
     return VoidResult.INSTANCE;
   }
 }
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 c98bd5d..13efa6c 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
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.assistedinject.Assisted;
@@ -249,7 +250,8 @@
     if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
       indexes.add(closedIndex);
     }
-    return new QuerySource(indexes, QueryBuilder.toQuery(p), limit);
+    return new QuerySource(indexes, QueryBuilder.toQuery(schema, p), limit,
+        ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
   }
 
   @Override
@@ -270,11 +272,14 @@
     private final List<SubIndex> indexes;
     private final Query query;
     private final int limit;
+    private final boolean reverse;
 
-    public QuerySource(List<SubIndex> indexes, Query query, int limit) {
+    private QuerySource(List<SubIndex> indexes, Query query, int limit,
+        boolean reverse) {
       this.indexes = indexes;
       this.query = query;
       this.limit = limit;
+      this.reverse = reverse;
     }
 
     @Override
@@ -297,9 +302,11 @@
       IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
       Sort sort = new Sort(
           new SortField(
-              ChangeField.UPDATED.getName(),
-              SortField.Type.INT,
-              true /* descending */));
+              ChangeField.SORTKEY.getName(),
+              SortField.Type.LONG,
+              // Standard order is descending by sort key, unless reversed due
+              // to a sortkey_before predicate.
+              !reverse));
       try {
         TopDocs[] hits = new TopDocs[indexes.size()];
         for (int i = 0; i < indexes.size(); i++) {
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 f491bc2..f99cce8 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
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.index.TimestampRangePredicate;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.NotPredicate;
@@ -54,26 +55,27 @@
     return intTerm(ID_FIELD, cd.getId().get());
   }
 
-  public static Query toQuery(Predicate<ChangeData> p)
+  public static Query toQuery(Schema<ChangeData> schema, Predicate<ChangeData> p)
       throws QueryParseException {
     if (p instanceof AndPredicate) {
-      return and(p);
+      return and(schema, p);
     } else if (p instanceof OrPredicate) {
-      return or(p);
+      return or(schema, p);
     } else if (p instanceof NotPredicate) {
-      return not(p);
+      return not(schema, p);
     } else if (p instanceof IndexPredicate) {
-      return fieldQuery((IndexPredicate<ChangeData>) p);
+      return fieldQuery(schema, (IndexPredicate<ChangeData>) p);
     } else {
       throw new QueryParseException("cannot create query for index: " + p);
     }
   }
 
-  private static Query or(Predicate<ChangeData> p) throws QueryParseException {
+  private static Query or(Schema<ChangeData> schema, Predicate<ChangeData> p)
+      throws QueryParseException {
     try {
       BooleanQuery q = new BooleanQuery();
       for (int i = 0; i < p.getChildCount(); i++) {
-        q.add(toQuery(p.getChild(i)), SHOULD);
+        q.add(toQuery(schema, p.getChild(i)), SHOULD);
       }
       return q;
     } catch (BooleanQuery.TooManyClauses e) {
@@ -81,7 +83,8 @@
     }
   }
 
-  private static Query and(Predicate<ChangeData> p) throws QueryParseException {
+  private static Query and(Schema<ChangeData> schema, Predicate<ChangeData> p)
+      throws QueryParseException {
     try {
       BooleanQuery b = new BooleanQuery();
       List<Query> not = Lists.newArrayListWithCapacity(p.getChildCount());
@@ -92,10 +95,10 @@
           if (n instanceof TimestampRangePredicate) {
             b.add(notTimestamp((TimestampRangePredicate<ChangeData>) n), MUST);
           } else {
-            not.add(toQuery(n));
+            not.add(toQuery(schema, n));
           }
         } else {
-          b.add(toQuery(c), MUST);
+          b.add(toQuery(schema, c), MUST);
         }
       }
       for (Query q : not) {
@@ -107,7 +110,8 @@
     }
   }
 
-  private static Query not(Predicate<ChangeData> p) throws QueryParseException {
+  private static Query not(Schema<ChangeData> schema, Predicate<ChangeData> p)
+      throws QueryParseException {
     Predicate<ChangeData> n = p.getChild(0);
     if (n instanceof TimestampRangePredicate) {
       return notTimestamp((TimestampRangePredicate<ChangeData>) n);
@@ -116,12 +120,12 @@
     // Lucene does not support negation, start with all and subtract.
     BooleanQuery q = new BooleanQuery();
     q.add(new MatchAllDocsQuery(), MUST);
-    q.add(toQuery(n), MUST_NOT);
+    q.add(toQuery(schema, n), MUST_NOT);
     return q;
   }
 
-  private static Query fieldQuery(IndexPredicate<ChangeData> p)
-      throws QueryParseException {
+  private static Query fieldQuery(Schema<ChangeData> schema,
+      IndexPredicate<ChangeData> p) throws QueryParseException {
     if (p.getType() == FieldType.INTEGER) {
       return intQuery(p);
     } else if (p.getType() == FieldType.TIMESTAMP) {
@@ -133,7 +137,7 @@
     } else if (p.getType() == FieldType.FULL_TEXT) {
       return fullTextQuery(p);
     } else if (p instanceof SortKeyPredicate) {
-      return sortKeyQuery((SortKeyPredicate) p);
+      return sortKeyQuery(schema, (SortKeyPredicate) p);
     } else {
       throw badFieldType(p.getType());
     }
@@ -158,12 +162,14 @@
     return new TermQuery(intTerm(p.getField().getName(), value));
   }
 
-  private static Query sortKeyQuery(SortKeyPredicate p) {
+  private static Query sortKeyQuery(Schema<ChangeData> schema, SortKeyPredicate p) {
+    long min = p.getMinValue(schema);
+    long max = p.getMaxValue(schema);
     return NumericRangeQuery.newLongRange(
         p.getField().getName(),
-        p.getMinValue() != Long.MIN_VALUE ? p.getMinValue() : null,
-        p.getMaxValue() != Long.MAX_VALUE ? p.getMaxValue() : null,
-        true, true);
+        min != Long.MIN_VALUE ? min : null,
+        max != Long.MAX_VALUE ? max : null,
+        false, false);
   }
 
   private static Query timestampQuery(IndexPredicate<ChangeData> p)
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
index b830959..4f77b0f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/BaseInit.java
@@ -77,7 +77,8 @@
     final SiteInit init = createSiteInit();
     beforeInit(init);
 
-    init.flags.autoStart = getAutoStart() && init.site.isNew;;
+    init.flags.autoStart = getAutoStart() && init.site.isNew;
+    init.flags.skipPlugins = skipPlugins();
 
     final SiteRun run;
     try {
@@ -103,6 +104,10 @@
     return 0;
   }
 
+  protected boolean skipPlugins() {
+    return false;
+  }
+
   protected void beforeInit(SiteInit init) throws Exception {
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index 1cab515..cbf50b9 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -126,6 +126,11 @@
     return !noAutoStart;
   }
 
+  @Override
+  protected boolean skipPlugins() {
+    return skipPlugins;
+  }
+
   void start(SiteRun run) throws Exception {
     if (run.flags.autoStart) {
       if (HostPlatform.isWin32()) {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index 92c479e..6cc83e5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -77,6 +77,11 @@
     UNIFIED_DIFF
   }
 
+  public static enum ChangeScreen {
+    OLD_UI,
+    CHANGE_SCREEN2
+  }
+
   public static enum TimeFormat {
     /** 12-hour clock: 1:15 am, 2:13 pm */
     HHMM_12("h:mm a"),
@@ -144,6 +149,9 @@
   @Column(id = 14, length = 20, notNull = false)
   protected String diffView;
 
+  @Column(id = 15, length = 20, notNull = false)
+  protected String changeScreen;
+
   public AccountGeneralPreferences() {
   }
 
@@ -278,6 +286,14 @@
     this.diffView = diffView.name();
   }
 
+  public ChangeScreen getChangeScreen() {
+    return changeScreen != null ? ChangeScreen.valueOf(changeScreen) : null;
+  }
+
+  public void setChangeScreen(ChangeScreen ui) {
+    changeScreen = ui != null ? ui.name() : null;
+  }
+
   public void resetToDefaults() {
     maximumPageSize = DEFAULT_PAGESIZE;
     showSiteHeader = true;
@@ -292,5 +308,6 @@
     relativeDateInChangeTable = false;
     commentVisibilityStrategy = null;
     diffView = null;
+    changeScreen = null;
   }
 }
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 1e20546..5232f41 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -1,4 +1,7 @@
-SRCS = glob(['src/main/java/**/*.java'])
+SRCS = glob([
+  'src/main/java/**/*.java',
+  'src/test/java/com/google/gerrit/server/project/Util.java'
+])
 RESOURCES =  glob(['src/main/resources/**/*'])
 
 # TODO(sop) break up gerrit-server java_library(), its too big
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 f79e05f..2f1cfa0dc 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
@@ -35,6 +35,8 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidators;
+
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.CommitMessageEditedSender;
 import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -392,19 +394,19 @@
     }
   }
 
-  public static void deleteDraftChange(final PatchSet.Id patchSetId,
+  public static void deleteDraftChange(PatchSet.Id patchSetId,
       GitRepositoryManager gitManager,
-      final GitReferenceUpdated gitRefUpdated, final ReviewDb db)
+      GitReferenceUpdated gitRefUpdated, ReviewDb db, ChangeIndexer indexer)
       throws NoSuchChangeException, OrmException, IOException {
     final Change.Id changeId = patchSetId.getParentKey();
-    deleteDraftChange(changeId, gitManager, gitRefUpdated, db);
+    deleteDraftChange(changeId, gitManager, gitRefUpdated, db, indexer);
   }
 
-  public static void deleteDraftChange(final Change.Id changeId,
+  public static void deleteDraftChange(Change.Id changeId,
       GitRepositoryManager gitManager,
-      final GitReferenceUpdated gitRefUpdated, final ReviewDb db)
+      GitReferenceUpdated gitRefUpdated, ReviewDb db, ChangeIndexer indexer)
       throws NoSuchChangeException, OrmException, IOException {
-    final Change change = db.changes().get(changeId);
+    Change change = db.changes().get(changeId);
     if (change == null || change.getStatus() != Change.Status.DRAFT) {
       throw new NoSuchChangeException(changeId);
     }
@@ -418,6 +420,7 @@
     db.starredChanges().delete(db.starredChanges().byChange(changeId));
     db.trackingIds().delete(db.trackingIds().byChange(changeId));
     db.changes().delete(Collections.singleton(change));
+    indexer.delete(change);
   }
 
   public static void deleteOnlyDraftPatchSet(final PatchSet patch,
@@ -475,11 +478,7 @@
     if ("z".equals(sortKey)) {
       return Long.MAX_VALUE;
     }
-    String ts = sortKey.substring(0, 8);
-    int i = 0;
-    while (i < 8 && ts.charAt(i) == '0')
-      i++;
-    return Long.parseLong(ts.substring(i), 16);
+    return Long.parseLong(sortKey, 16);
   }
 
   public static void computeSortKey(final Change c) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
index 733c49d..4d3b913 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.ChangeScreen;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DateFormat;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView;
@@ -61,6 +62,7 @@
     Boolean relativeDateInChangeTable;
     CommentVisibilityStrategy commentVisibilityStrategy;
     DiffView diffView;
+    ChangeScreen changeScreen;
 
     PreferenceInfo(AccountGeneralPreferences p) {
       changesPerPage = p.getMaximumPageSize();
@@ -76,6 +78,7 @@
       relativeDateInChangeTable = p.isRelativeDateInChangeTable() ? true : null;
       commentVisibilityStrategy = p.getCommentVisibilityStrategy();
       diffView = p.getDiffView();
+      changeScreen = p.getChangeScreen();
     }
   }
 }
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 8b7b6ad..2c20b03 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
@@ -129,13 +129,19 @@
             currentUser.newCommitterIdent(myIdent.getWhen(),
                 myIdent.getTimeZone());
 
+        final ObjectId computedChangeId =
+            ChangeIdUtil
+                .computeChangeId(commitToCherryPick.getTree(), mergeTip,
+                    commitToCherryPick.getAuthorIdent(), myIdent, message);
+        String commitMessage = ChangeIdUtil.insertId(message, computedChangeId);
+
         RevCommit cherryPickCommit;
         ObjectInserter oi = git.newObjectInserter();
         try {
           ProjectState projectState = refControl.getProjectControl().getProjectState();
           cherryPickCommit =
               mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
-                  commitToCherryPick, committerIdent, message, revWalk);
+                  commitToCherryPick, committerIdent, commitMessage, revWalk);
         } finally {
           oi.release();
         }
@@ -151,11 +157,6 @@
           final String idStr = idList.get(idList.size() - 1).trim();
           changeKey = new Change.Key(idStr);
         } else {
-          final ObjectId computedChangeId =
-              ChangeIdUtil
-                  .computeChangeId(cherryPickCommit.getTree(), mergeTip,
-                      cherryPickCommit.getAuthorIdent(), myIdent, message);
-
           changeKey = new Change.Key("I" + computedChangeId.name());
         }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
index 7d3a012..1386e0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftChange.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.change.DeleteDraftChange.Input;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
@@ -42,15 +43,18 @@
   protected final Provider<ReviewDb> dbProvider;
   private final GitRepositoryManager gitManager;
   private final GitReferenceUpdated gitRefUpdated;
+  private final ChangeIndexer indexer;
 
   @Inject
   public DeleteDraftChange(Provider<ReviewDb> dbProvider,
       GitRepositoryManager gitManager,
       GitReferenceUpdated gitRefUpdated,
-      PatchSetInfoFactory patchSetInfoFactory) {
+      PatchSetInfoFactory patchSetInfoFactory,
+      ChangeIndexer indexer) {
     this.dbProvider = dbProvider;
     this.gitManager = gitManager;
     this.gitRefUpdated = gitRefUpdated;
+    this.indexer = indexer;
   }
 
   @Override
@@ -67,7 +71,7 @@
 
     try {
       ChangeUtil.deleteDraftChange(rsrc.getChange().getId(),
-          gitManager, gitRefUpdated, dbProvider.get());
+          gitManager, gitRefUpdated, dbProvider.get(), indexer);
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(e.getMessage());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index d4e17d6..9838381 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.change.DeleteDraftPatchSet.Input;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -47,16 +48,19 @@
   private final GitRepositoryManager gitManager;
   private final GitReferenceUpdated gitRefUpdated;
   private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ChangeIndexer indexer;
 
   @Inject
   public DeleteDraftPatchSet(Provider<ReviewDb> dbProvider,
       GitRepositoryManager gitManager,
       GitReferenceUpdated gitRefUpdated,
-      PatchSetInfoFactory patchSetInfoFactory) {
+      PatchSetInfoFactory patchSetInfoFactory,
+      ChangeIndexer indexer) {
     this.dbProvider = dbProvider;
     this.gitManager = gitManager;
     this.gitRefUpdated = gitRefUpdated;
     this.patchSetInfoFactory = patchSetInfoFactory;
+    this.indexer = indexer;
   }
 
   @Override
@@ -129,7 +133,7 @@
       throws OrmException, IOException, ResourceNotFoundException {
     try {
       ChangeUtil.deleteDraftChange(patchSetId,
-          gitManager, gitRefUpdated, dbProvider.get());
+          gitManager, gitRefUpdated, dbProvider.get(), indexer);
     } catch (NoSuchChangeException e) {
       throw new ResourceNotFoundException(e.getMessage());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 25bd80f..d1fd730 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -59,6 +59,7 @@
     post(CHANGE_KIND, "index").to(Index.class);
 
     post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
+    get(CHANGE_KIND, "suggest_reviewers").to(SuggestReviewers.class);
     child(CHANGE_KIND, "reviewers").to(Reviewers.class);
     get(REVIEWER_KIND).to(GetReviewer.class);
     delete(REVIEWER_KIND).to(DeleteReviewer.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
new file mode 100644
index 0000000..1a670cc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -0,0 +1,296 @@
+// 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.change;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.Project;
+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.AccountCache;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountVisibility;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupJson.GroupBaseInfo;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+class SuggestReviewers implements RestReadView<ChangeResource> {
+
+  private static final String MAX_SUFFIX = "\u9fa5";
+  private static final int MAX = 10;
+
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final AccountControl.Factory accountControlFactory;
+  private final GroupMembers.Factory groupMembersFactory;
+  private final AccountCache accountCache;
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<CurrentUser> currentUser;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final GroupBackend groupBackend;
+  private final boolean suggestAccounts;
+  private final int suggestFrom;
+  private final int maxAllowed;
+  private int limit;
+  private String query;
+
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT",
+      usage = "maximum number of reviewers to list")
+  public void setLimit(int l) {
+    this.limit = l <= 0 ? MAX : Math.min(l, MAX);
+  }
+
+  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY",
+      usage = "match reviewers query")
+  public void setQuery(String q) {
+    this.query = q;
+  }
+
+  @Inject
+  SuggestReviewers(AccountVisibility av,
+      AccountInfo.Loader.Factory accountLoaderFactory,
+      AccountControl.Factory accountControlFactory,
+      AccountCache accountCache,
+      GroupMembers.Factory groupMembersFactory,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      Provider<CurrentUser> currentUser,
+      Provider<ReviewDb> dbProvider,
+      @GerritServerConfig Config cfg,
+      GroupBackend groupBackend) {
+    this.accountLoaderFactory = accountLoaderFactory;
+    this.accountControlFactory = accountControlFactory;
+    this.accountCache = accountCache;
+    this.groupMembersFactory = groupMembersFactory;
+    this.dbProvider = dbProvider;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.currentUser = currentUser;
+    this.groupBackend = groupBackend;
+
+    String suggest = cfg.getString("suggest", null, "accounts");
+    if ("OFF".equalsIgnoreCase(suggest)
+        || "false".equalsIgnoreCase(suggest)) {
+      this.suggestAccounts = false;
+    } else {
+      this.suggestAccounts = (av != AccountVisibility.NONE);
+    }
+
+    this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
+    this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed",
+        PostReviewers.DEFAULT_MAX_REVIEWERS);
+  }
+
+  private interface VisibilityControl {
+    boolean isVisibleTo(Account account) throws OrmException;
+  }
+
+  @Override
+  public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
+      throws BadRequestException, OrmException, IOException {
+    if (Strings.isNullOrEmpty(query)) {
+      throw new BadRequestException("missing query field");
+    }
+
+    if (!suggestAccounts || query.length() < suggestFrom) {
+      return Collections.emptyList();
+    }
+
+    VisibilityControl visibilityControl = getVisibility(rsrc);
+    List<AccountInfo> suggestedAccounts = suggestAccount(visibilityControl);
+    accountLoaderFactory.create(true).fill(suggestedAccounts);
+
+    List<SuggestedReviewerInfo> reviewer = Lists.newArrayList();
+    for (AccountInfo a : suggestedAccounts) {
+      reviewer.add(new SuggestedReviewerInfo(a));
+    }
+
+    Project p = rsrc.getControl().getProject();
+    for (GroupReference g : suggestAccountGroup(
+        rsrc.getControl().getProjectControl())) {
+      if (suggestGroupAsReviewer(p, g, visibilityControl)) {
+        GroupBaseInfo info = new GroupBaseInfo();
+        info.id = Url.encode(g.getUUID().get());
+        info.name = g.getName();
+        reviewer.add(new SuggestedReviewerInfo(info));
+      }
+    }
+
+    Collections.sort(reviewer);
+    if (reviewer.size() <= limit) {
+      return reviewer;
+    } else {
+      return reviewer.subList(0, limit);
+    }
+  }
+
+  private VisibilityControl getVisibility(final ChangeResource rsrc) {
+    if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) {
+      return new VisibilityControl() {
+        @Override
+        public boolean isVisibleTo(Account account) throws OrmException {
+          return true;
+        }
+      };
+    } else {
+      return new VisibilityControl() {
+        @Override
+        public boolean isVisibleTo(Account account) throws OrmException {
+          IdentifiedUser who =
+              identifiedUserFactory.create(dbProvider, account.getId());
+          // we can't use changeControl directly as it won't suggest reviewers
+          // to drafts
+          return rsrc.getControl().forUser(who).isRefVisible();
+        }
+      };
+    }
+  }
+
+  private List<GroupReference> suggestAccountGroup(ProjectControl ctl) {
+    return Lists.newArrayList(
+        Iterables.limit(groupBackend.suggest(query, ctl), limit));
+  }
+
+  private List<AccountInfo> suggestAccount(VisibilityControl visibilityControl)
+      throws OrmException {
+    String a = query;
+    String b = a + MAX_SUFFIX;
+
+    LinkedHashMap<Account.Id, AccountInfo> r = Maps.newLinkedHashMap();
+    for (Account p : dbProvider.get().accounts()
+        .suggestByFullName(a, b, limit)) {
+      addSuggestion(r, p, new AccountInfo(p.getId()), visibilityControl);
+    }
+
+    if (r.size() < limit) {
+      for (Account p : dbProvider.get().accounts()
+          .suggestByPreferredEmail(a, b, limit - r.size())) {
+        addSuggestion(r, p, new AccountInfo(p.getId()), visibilityControl);
+      }
+    }
+
+    if (r.size() < limit) {
+      for (AccountExternalId e : dbProvider.get().accountExternalIds()
+          .suggestByEmailAddress(a, b, limit - r.size())) {
+        if (!r.containsKey(e.getAccountId())) {
+          Account p = accountCache.get(e.getAccountId()).getAccount();
+          AccountInfo info = new AccountInfo(p.getId());
+          addSuggestion(r, p, info, visibilityControl);
+        }
+      }
+    }
+
+    return Lists.newArrayList(r.values());
+  }
+
+  private void addSuggestion(Map<Account.Id, AccountInfo> map, Account account,
+      AccountInfo info, VisibilityControl visibilityControl)
+      throws OrmException {
+    if (!map.containsKey(account.getId())
+        && account.isActive()
+        // Can the suggestion see the change?
+        && visibilityControl.isVisibleTo(account)
+        // Can the account see the current user?
+        && accountControlFactory.get().canSee(account)) {
+      map.put(account.getId(), info);
+    }
+  }
+
+  private boolean suggestGroupAsReviewer(Project project,
+      GroupReference group, VisibilityControl visibilityControl)
+      throws OrmException, IOException {
+    if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
+      return false;
+    }
+
+    try {
+      Set<Account> members = groupMembersFactory
+          .create(currentUser.get())
+          .listAccounts(group.getUUID(), project.getNameKey());
+
+      if (members.isEmpty()) {
+        return false;
+      }
+
+      if (maxAllowed > 0 && members.size() > maxAllowed) {
+        return false;
+      }
+
+      // require that at least one member in the group can see the change
+      for (Account account : members) {
+        if (visibilityControl.isVisibleTo(account)) {
+          return true;
+        }
+      }
+    } catch (NoSuchGroupException e) {
+      return false;
+    } catch (NoSuchProjectException e) {
+      return false;
+    }
+
+    return false;
+  }
+
+  static class SuggestedReviewerInfo implements Comparable<SuggestedReviewerInfo> {
+    String kind = "gerritcodereview#suggestedreviewer";
+    AccountInfo account;
+    GroupBaseInfo group;
+
+    SuggestedReviewerInfo(AccountInfo a) {
+      this.account = a;
+    }
+
+    SuggestedReviewerInfo(GroupBaseInfo g) {
+      this.group = g;
+    }
+
+    @Override
+    public int compareTo(SuggestedReviewerInfo o) {
+      return getSortValue().compareTo(o.getSortValue());
+    }
+
+    private String getSortValue() {
+      return account != null
+          ? Objects.firstNonNull(account.email,
+              Strings.nullToEmpty(account.name))
+          : Strings.nullToEmpty(group.name);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
index d20b0f6..e6ce4c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -46,6 +47,7 @@
   private final GitRepositoryManager gitManager;
   private final GitReferenceUpdated gitRefUpdated;
   private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ChangeIndexer indexer;
 
   private final PatchSet.Id patchSetId;
 
@@ -53,12 +55,14 @@
   DeleteDraftPatchSet(ChangeControl.Factory changeControlFactory,
       ReviewDb db, GitRepositoryManager gitManager,
       GitReferenceUpdated gitRefUpdated, PatchSetInfoFactory patchSetInfoFactory,
+      ChangeIndexer indexer,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.gitManager = gitManager;
     this.gitRefUpdated = gitRefUpdated;
     this.patchSetInfoFactory = patchSetInfoFactory;
+    this.indexer = indexer;
 
     this.patchSetId = patchSetId;
   }
@@ -97,7 +101,8 @@
     List<PatchSet> restOfPatches = db.patchSets().byChange(changeId).toList();
     if (restOfPatches.size() == 0) {
       try {
-        ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db);
+        ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db,
+            indexer);
         result.setChangeId(null);
       } catch (IOException e) {
         result.addError(new ReviewResult.Error(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java
index 493a40f..2992bd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java
@@ -25,10 +25,15 @@
   }
 
   @Override
-  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+  protected CodeReviewCommit _run(CodeReviewCommit mergeTip,
+      List<CodeReviewCommit> toMerge) throws MergeException {
     args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
 
+    if (mergeTip == null) {
+      // The branch is unborn. Take a fast-forward resolution to
+      // create the branch.
+      mergeTip = toMerge.remove(0);
+    }
     CodeReviewCommit newMergeTip = mergeTip;
     while (!toMerge.isEmpty()) {
       newMergeTip =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java
index c31edb2..3a326fc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java
@@ -25,9 +25,16 @@
   }
 
   @Override
-  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+  protected CodeReviewCommit _run(CodeReviewCommit mergeTip,
+      List<CodeReviewCommit> toMerge) throws MergeException {
     args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
+
+    if (mergeTip == null) {
+      // The branch is unborn. Take a fast-forward resolution to
+      // create the branch.
+      mergeTip = toMerge.remove(0);
+    }
+
     CodeReviewCommit newMergeTip =
         args.mergeUtil.getFirstFastForward(mergeTip, args.rw, toMerge);
 
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 5245957..673c528 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
@@ -1253,6 +1253,11 @@
       walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId()));
       if (magicBranch.baseCommit != null) {
         walk.markUninteresting(magicBranch.baseCommit);
+        assert magicBranch.ctl != null;
+        Ref targetRef = allRefs.get(magicBranch.ctl.getRefName());
+        if (targetRef != null) {
+          walk.markUninteresting(walk.parseCommit(targetRef.getObjectId()));
+        }
       } else {
         markHeadsAsUninteresting(
             walk,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
index a74e6ef..8c9056d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
@@ -126,10 +126,8 @@
     }
   }
 
-  public static class GroupInfo {
+  public static class GroupInfo extends GroupBaseInfo {
     final String kind = "gerritcodereview#group";
-    public String id;
-    public String name;
     public String url;
     public GroupOptionsInfo options;
 
@@ -143,4 +141,9 @@
     public List<AccountInfo> members;
     public List<GroupInfo> includes;
   }
+
+  public static class GroupBaseInfo {
+    public String id;
+    public String name;
+  }
 }
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 20b61e7..cfa4644 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
@@ -125,8 +125,32 @@
         }
       };
 
-  /** Sort key field, duplicates {@link #UPDATED}. */
   @Deprecated
+  public static long legacyParseSortKey(String sortKey) {
+    if ("z".equals(sortKey)) {
+      return Long.MAX_VALUE;
+    }
+    return Long.parseLong(sortKey.substring(0, 8), 16);
+  }
+
+  /** Legacy sort key field. */
+  @Deprecated
+  public static final FieldDef<ChangeData, Long> LEGACY_SORTKEY =
+      new FieldDef.Single<ChangeData, Long>(
+          "sortkey", FieldType.LONG, true) {
+        @Override
+        public Long get(ChangeData input, FillArgs args)
+            throws OrmException {
+          return legacyParseSortKey(input.change(args.db).getSortKey());
+        }
+      };
+
+  /**
+   * Sort key field.
+   * <p>
+   * Redundant with {@link #UPDATED} and {@link #LEGACY_ID}, but secondary index
+   * implementations may not be able to search over tuples of values.
+   */
   public static final FieldDef<ChangeData, Long> SORTKEY =
       new FieldDef.Single<ChangeData, Long>(
           "sortkey", 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 b67faa3..8e89507 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
@@ -132,7 +132,11 @@
    *     or NOT predicates as internal nodes, and {@link IndexPredicate}s as
    *     leaves.
    * @param limit maximum number of results to return.
-   * @return a source of documents matching the predicate.
+   * @return a source of documents matching the predicate. Documents must be
+   *     returned in descending sort key order, unless a {@code sortkey_after}
+   *     predicate (with a cut point not at {@link Long#MAX_VALUE}) is provided,
+   *     in which case the source should return documents in ascending sort key
+   *     order starting from the sort key cut point.
    *
    * @throws QueryParseException if the predicate could not be converted to an
    *     indexed data source.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
index 0aa3fc8..451b0d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexer.java
@@ -41,6 +41,7 @@
       return Futures.immediateFuture(null);
     }
 
+    @Override
     public Callable<Void> indexTask(ChangeData cd) {
       return new Callable<Void>() {
         @Override
@@ -49,6 +50,16 @@
         }
       };
     }
+
+    @Override
+    public Callable<Void> deleteTask(ChangeData cd) {
+      return new Callable<Void>() {
+        @Override
+        public Void call() {
+          return null;
+        }
+      };
+    }
   };
 
   private final ListeningScheduledExecutorService executor;
@@ -71,12 +82,45 @@
    * Start indexing a change.
    *
    * @param change change to index.
-   * @param prop propagator to wrap any created runnables in.
    * @return future for the indexing task.
    */
   public ListenableFuture<?> index(ChangeData cd) {
     return executor.submit(indexTask(cd));
   }
 
+  /**
+   * Create a runnable to index a change.
+   *
+   * @param cd change to index.
+   * @return unstarted runnable to index the change.
+   */
   public abstract Callable<Void> indexTask(ChangeData cd);
+
+  /**
+   * Start deleting a change.
+   *
+   * @param change change to delete.
+   * @return future for the deleting task.
+   */
+  public ListenableFuture<?> delete(Change change) {
+    return delete(new ChangeData(change));
+  }
+
+  /**
+   * Start deleting a change.
+   *
+   * @param change change to delete.
+   * @return future for the deleting task.
+   */
+  public ListenableFuture<?> delete(ChangeData cd) {
+    return executor.submit(deleteTask(cd));
+  }
+
+  /**
+   * Create a runnable to delete a change.
+   *
+   * @param cd change to delete.
+   * @return unstarted runnable to delete the change.
+   */
+  public abstract Callable<Void> deleteTask(ChangeData cd);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
index 1de5c99..715e96b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndexerImpl.java
@@ -30,6 +30,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.concurrent.Callable;
 
 /**
@@ -73,14 +74,21 @@
 
   @Override
   public Callable<Void> indexTask(ChangeData cd) {
-    return new Task(cd);
+    return new Task(cd, false);
+  }
+
+  @Override
+  public Callable<Void> deleteTask(ChangeData cd) {
+    return new Task(cd, true);
   }
 
   private class Task implements Callable<Void> {
     private final ChangeData cd;
+    private final boolean delete;
 
-    private Task(ChangeData cd) {
+    private Task(ChangeData cd, boolean delete) {
       this.cd = cd;
+      this.delete = delete;
     }
 
     @Override
@@ -101,10 +109,10 @@
           });
           if (indexes != null) {
             for (ChangeIndex i : indexes.getWriteIndexes()) {
-              i.replace(cd); // TODO(dborowitz): Parallelize these
+              apply(i, cd); // TODO(dborowitz): Parallelize these
             }
           } else {
-            index.replace(cd);
+            apply(index, cd);
           }
           return null;
         } finally  {
@@ -119,6 +127,14 @@
       }
     }
 
+    private void apply(ChangeIndex i, ChangeData cd) throws IOException {
+      if (delete) {
+        i.delete(cd);
+      } else {
+        i.replace(cd);
+      }
+    }
+
     @Override
     public String toString() {
       return "index-change-" + cd.getId().get();
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 1ed6c47..39a29fb 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.REF,
         ChangeField.TOPIC,
         ChangeField.UPDATED,
-        ChangeField.SORTKEY,
+        ChangeField.LEGACY_SORTKEY,
         ChangeField.FILE,
         ChangeField.OWNER,
         ChangeField.REVIEWER,
@@ -58,6 +58,28 @@
         ChangeField.REF,
         ChangeField.TOPIC,
         ChangeField.UPDATED,
+        ChangeField.LEGACY_SORTKEY,
+        ChangeField.FILE,
+        ChangeField.OWNER,
+        ChangeField.REVIEWER,
+        ChangeField.COMMIT,
+        ChangeField.TR,
+        ChangeField.LABEL,
+        ChangeField.REVIEWED,
+        ChangeField.COMMIT_MESSAGE,
+        ChangeField.COMMENT,
+        ChangeField.CHANGE,
+        ChangeField.APPROVAL);
+
+  @SuppressWarnings("unchecked")
+  static final Schema<ChangeData> V3 = release(
+        ChangeField.LEGACY_ID,
+        ChangeField.ID,
+        ChangeField.STATUS,
+        ChangeField.PROJECT,
+        ChangeField.REF,
+        ChangeField.TOPIC,
+        ChangeField.UPDATED,
         ChangeField.SORTKEY,
         ChangeField.FILE,
         ChangeField.OWNER,
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 aab6c64..bbc4773 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
@@ -154,7 +154,7 @@
 
     Predicate<ChangeData> out = rewriteImpl(in, index, limit);
     if (in == out || out instanceof IndexPredicate) {
-      return new IndexedChangeQuery(index, out, limit);
+      return new IndexedChangeQuery(db, index, out, limit);
     } else if (out == null /* cannot rewrite */) {
       return in;
     } else {
@@ -231,7 +231,7 @@
     if (isIndexed.cardinality() == 1) {
       int i = isIndexed.nextSetBit(0);
       newChildren.add(
-          0, new IndexedChangeQuery(index, newChildren.remove(i), limit));
+          0, new IndexedChangeQuery(db, index, newChildren.remove(i), limit));
       return copy(in, newChildren);
     }
 
@@ -251,7 +251,7 @@
         all.add(c);
       }
     }
-    all.add(0, new IndexedChangeQuery(index, in.copy(indexed), limit));
+    all.add(0, new IndexedChangeQuery(db, index, in.copy(indexed), limit));
     return copy(in, all);
   }
 
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 ccd6c89..2185af3 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
@@ -14,17 +14,22 @@
 
 package com.google.gerrit.server.index;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
-
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 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.ChangeDataSource;
+import com.google.gerrit.server.query.change.Paginated;
+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;
 
 import java.util.Collection;
 import java.util.Iterator;
@@ -39,16 +44,55 @@
  * {@link ChangeDataSource} to be chosen by the query processor.
  */
 public class IndexedChangeQuery extends Predicate<ChangeData>
-    implements ChangeDataSource {
-  private final Predicate<ChangeData> pred;
-  private final int limit;
-  private final ChangeDataSource source;
+    implements ChangeDataSource, Paginated {
 
-  public IndexedChangeQuery(ChangeIndex index, Predicate<ChangeData> pred, int limit)
-      throws QueryParseException {
+  /**
+   * Replace all {@link SortKeyPredicate}s in a tree.
+   * <p>
+   * Strictly speaking this should replace only the {@link SortKeyPredicate} at
+   * the top-level AND node, but this implementation is simpler, and the
+   * behavior of having multiple sortkey operators is undefined anyway.
+   *
+   * @param p predicate to replace in.
+   * @param newValue new cut value to replace all sortkey operators with.
+   * @return a copy of {@code p} with all sortkey predicates replaced; or p
+   *     itself.
+   */
+  @VisibleForTesting
+  static Predicate<ChangeData> replaceSortKeyPredicates(
+      Predicate<ChangeData> p, String newValue) {
+    if (p instanceof SortKeyPredicate) {
+      return ((SortKeyPredicate) p).copy(newValue);
+    } else if (p.getChildCount() > 0) {
+      List<Predicate<ChangeData>> newChildren =
+          Lists.newArrayListWithCapacity(p.getChildCount());
+      boolean replaced = false;
+      for (Predicate<ChangeData> c : p.getChildren()) {
+        Predicate<ChangeData> nc = replaceSortKeyPredicates(c, newValue);
+        newChildren.add(nc);
+        if (nc != c) {
+          replaced = true;
+        }
+      }
+      return replaced ? p.copy(newChildren) : p;
+    } else {
+      return p;
+    }
+  }
+
+  private final Provider<ReviewDb> db;
+  private final ChangeIndex index;
+  private final int limit;
+
+  private Predicate<ChangeData> pred;
+  private ChangeDataSource source;
+
+  public IndexedChangeQuery(Provider<ReviewDb> db, ChangeIndex index,
+      Predicate<ChangeData> pred, int limit) throws QueryParseException {
+    this.db = db;
+    this.index = index;
     this.pred = pred;
     this.limit = limit;
-    this.source = index.getSource(pred, limit);
   }
 
   @Override
@@ -70,18 +114,32 @@
   }
 
   @Override
+  public int limit() {
+    return limit;
+  }
+
+  @Override
   public int getCardinality() {
-    return source.getCardinality();
+    return source != null ? source.getCardinality() : limit();
   }
 
   @Override
   public boolean hasChange() {
-    return source.hasChange();
+    return index.getSchema().getFields()
+        .containsKey(ChangeField.CHANGE.getName());
   }
 
   @Override
   public ResultSet<ChangeData> read() throws OrmException {
+    final ChangeDataSource currSource;
+    try {
+      currSource = index.getSource(pred, limit);
+    } catch (QueryParseException e) {
+      throw new OrmException(e);
+    }
+    source = currSource;
     final ResultSet<ChangeData> rs = source.read();
+
     return new ResultSet<ChangeData>() {
       @Override
       public Iterator<ChangeData> iterator() {
@@ -91,7 +149,7 @@
               @Override
               public
               ChangeData apply(ChangeData input) {
-                input.cacheFromSource(source);
+                input.cacheFromSource(currSource);
                 return input;
               }
             }).iterator();
@@ -101,7 +159,7 @@
       public List<ChangeData> toList() {
         List<ChangeData> r = rs.toList();
         for (ChangeData cd : r) {
-          cd.cacheFromSource(source);
+          cd.cacheFromSource(currSource);
         }
         return r;
       }
@@ -114,6 +172,12 @@
   }
 
   @Override
+  public ResultSet<ChangeData> restart(ChangeData last) throws OrmException {
+    pred = replaceSortKeyPredicates(pred, last.change(db).getSortKey());
+    return read();
+  }
+
+  @Override
   public Predicate<ChangeData> copy(
       Collection<? extends Predicate<ChangeData>> children) {
     return this;
@@ -121,7 +185,7 @@
 
   @Override
   public boolean match(ChangeData cd) throws OrmException {
-    return cd.isFromSource(source) || pred.match(cd);
+    return (source != null && cd.isFromSource(source)) || pred.match(cd);
   }
 
   @Override
@@ -150,7 +214,7 @@
   @Override
   public String toString() {
     return Objects.toStringHelper("index")
-        .add("p", source)
+        .add("p", pred)
         .add("limit", limit)
         .toString();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index fa2c281..d61fea4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -131,32 +131,34 @@
 
   public void installPluginFromStream(String name, InputStream in)
       throws IOException, PluginInstallException {
-    if (!name.endsWith(".jar")) {
-      name += ".jar";
+    String fileName = name;
+    if (!fileName.endsWith(".jar")) {
+      fileName += ".jar";
     }
 
-    File jar = new File(pluginsDir, name);
-    name = nameOf(jar);
+    File dst = new File(pluginsDir, fileName);
+    name = nameOf(dst);
 
-    File old = new File(pluginsDir, ".last_" + name + ".zip");
-    File tmp = asTemp(in, ".next_" + name, ".zip", pluginsDir);
+    File tmp = asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
     synchronized (this) {
       Plugin active = running.get(name);
       if (active != null) {
-        log.info(String.format("Replacing plugin %s", name));
+        fileName = active.getSrcFile().getName();
+        log.info(String.format("Replacing plugin %s", active.getName()));
+        File old = new File(pluginsDir, ".last_" + fileName);
         old.delete();
-        jar.renameTo(old);
+        active.getSrcFile().renameTo(old);
       }
 
-      new File(pluginsDir, name + ".jar.disabled").delete();
-      tmp.renameTo(jar);
+      new File(pluginsDir, fileName + ".disabled").delete();
+      tmp.renameTo(dst);
       try {
-        runPlugin(name, jar, active);
+        Plugin plugin = runPlugin(name, dst, active);
         if (active == null) {
-          log.info(String.format("Installed plugin %s", name));
+          log.info(String.format("Installed plugin %s", plugin.getName()));
         }
       } catch (PluginInstallException e) {
-        jar.delete();
+        dst.delete();
         throw e;
       }
 
@@ -214,8 +216,8 @@
           continue;
         }
 
-        log.info(String.format("Disabling plugin %s", name));
-        File off = new File(pluginsDir, active.getName() + ".jar.disabled");
+        log.info(String.format("Disabling plugin %s", active.getName()));
+        File off = new File(active.getSrcFile() + ".disabled");
         active.getSrcFile().renameTo(off);
 
         unloadPlugin(active);
@@ -225,7 +227,8 @@
           disabled.put(name, offPlugin);
         } catch (Throwable e) {
           // This shouldn't happen, as the plugin was loaded earlier.
-          log.warn(String.format("Cannot load disabled plugin %s", name),
+          log.warn(String.format(
+              "Cannot load disabled plugin %s", active.getName()),
               e.getCause());
         }
       }
@@ -242,7 +245,11 @@
         }
 
         log.info(String.format("Enabling plugin %s", name));
-        File on = new File(pluginsDir, off.getName() + ".jar");
+        String n = off.getSrcFile().getName();
+        if (n.endsWith(".disabled")) {
+          n = n.substring(0, n.lastIndexOf('.'));
+        }
+        File on = new File(pluginsDir, n);
         off.getSrcFile().renameTo(on);
 
         disabled.remove(name);
@@ -339,13 +346,13 @@
       }
 
       if (active != null) {
-        log.info(String.format("Reloading plugin %s", name));
+        log.info(String.format("Reloading plugin %s", active.getName()));
       }
 
       try {
         Plugin loadedPlugin = runPlugin(name, jar, active);
         if (active == null && !loadedPlugin.isDisabled()) {
-          log.info(String.format("Loaded plugin %s", name));
+          log.info(String.format("Loaded plugin %s", loadedPlugin.getName()));
         }
       } catch (PluginInstallException e) {
         log.warn(String.format("Cannot load plugin %s", name), e.getCause());
@@ -532,6 +539,8 @@
       public boolean accept(File pathname) {
         String n = pathname.getName();
         return (n.endsWith(".jar") || n.endsWith(".jar.disabled"))
+            && !n.startsWith(".last_")
+            && !n.startsWith(".next_")
             && pathname.isFile();
       }
     });
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 7684484..39abb59 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 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.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
@@ -121,6 +122,13 @@
     return ((IntPredicate<?>) find(p, IntPredicate.class, FIELD_LIMIT)).intValue();
   }
 
+  public static boolean hasNonTrivialSortKeyAfter(Schema<ChangeData> schema,
+      Predicate<ChangeData> p) {
+    SortKeyPredicate after =
+        (SortKeyPredicate) find(p, SortKeyPredicate.class, "sortkey_after");
+    return after != null && after.getMaxValue(schema) > 0;
+  }
+
   public static boolean hasSortKey(Predicate<ChangeData> p) {
     return find(p, SortKeyPredicate.class, "sortkey_after") != null
         || find(p, SortKeyPredicate.class, "sortkey_before") != null;
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 2b3edf7..d9ff80c 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
@@ -17,7 +17,7 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 
-interface Paginated {
+public interface Paginated {
   int limit();
 
   ResultSet<ChangeData> restart(ChangeData last) throws OrmException;
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 35a42f6..a502746 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
@@ -18,14 +18,25 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexPredicate;
+import com.google.gerrit.server.index.Schema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 public abstract class SortKeyPredicate extends IndexPredicate<ChangeData> {
+  @SuppressWarnings("deprecation")
+  private static long parseSortKey(Schema<ChangeData> schema, String value) {
+    FieldDef<ChangeData, ?> field = schema.getFields().get(ChangeField.SORTKEY.getName());
+    if (field == ChangeField.SORTKEY) {
+      return ChangeUtil.parseSortKey(value);
+    } else {
+      return ChangeField.legacyParseSortKey(value);
+    }
+  }
+
   protected final Provider<ReviewDb> dbProvider;
 
-  @SuppressWarnings("deprecation")
   SortKeyPredicate(Provider<ReviewDb> dbProvider, String name, String value) {
     super(ChangeField.SORTKEY, name, value);
     this.dbProvider = dbProvider;
@@ -36,8 +47,9 @@
     return 1;
   }
 
-  public abstract long getMinValue();
-  public abstract long getMaxValue();
+  public abstract long getMinValue(Schema<ChangeData> schema);
+  public abstract long getMaxValue(Schema<ChangeData> schema);
+  public abstract SortKeyPredicate copy(String newValue);
 
   public static class Before extends SortKeyPredicate {
     Before(Provider<ReviewDb> dbProvider, String value) {
@@ -45,13 +57,13 @@
     }
 
     @Override
-    public long getMinValue() {
+    public long getMinValue(Schema<ChangeData> schema) {
       return 0;
     }
 
     @Override
-    public long getMaxValue() {
-      return ChangeUtil.parseSortKey(getValue());
+    public long getMaxValue(Schema<ChangeData> schema) {
+      return parseSortKey(schema, getValue());
     }
 
     @Override
@@ -59,6 +71,11 @@
       Change change = cd.change(dbProvider);
       return change != null && change.getSortKey().compareTo(getValue()) < 0;
     }
+
+    @Override
+    public Before copy(String newValue) {
+      return new Before(dbProvider, newValue);
+    }
   }
 
   public static class After extends SortKeyPredicate {
@@ -67,12 +84,12 @@
     }
 
     @Override
-    public long getMinValue() {
-      return ChangeUtil.parseSortKey(getValue());
+    public long getMinValue(Schema<ChangeData> schema) {
+      return parseSortKey(schema, getValue());
     }
 
     @Override
-    public long getMaxValue() {
+    public long getMaxValue(Schema<ChangeData> schema) {
       return Long.MAX_VALUE;
     }
 
@@ -81,5 +98,10 @@
       Change change = cd.change(dbProvider);
       return change != null && change.getSortKey().compareTo(getValue()) > 0;
     }
+
+    @Override
+    public After copy(String newValue) {
+      return new After(dbProvider, newValue);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 99bc817..2882d00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_83> C = Schema_83.class;
+  public static final Class<Schema_84> C = Schema_84.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_84.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_84.java
new file mode 100644
index 0000000..c96f650
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_84.java
@@ -0,0 +1,26 @@
+// 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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_84 extends SchemaVersion {
+
+  @Inject
+  Schema_84(Provider<Schema_83> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index 6c605d2..4cd05ea 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -14,137 +14,77 @@
 
 package com.google.gerrit.rules;
 
+import static com.google.gerrit.common.data.Permission.LABEL;
+import static com.google.gerrit.server.project.Util.value;
+import static com.google.gerrit.server.project.Util.category;
+import static com.google.gerrit.server.project.Util.REGISTERED;
+import static com.google.gerrit.server.project.Util.grant;
+
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.Util;
 import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.AbstractModule;
 
 import java.util.Arrays;
-import java.util.Set;
 
 public class GerritCommonTest extends PrologTestCase {
-  private Projects projects;
+  private final LabelType V = category("Verified",
+      value(1, "Verified"),
+      value(0, "No score"),
+      value(-1, "Fails"));
+  private final LabelType Q = category("Qualified",
+      value(1, "Qualified"),
+      value(0, "No score"),
+      value(-1, "Fails"));
+
+  private final Project.NameKey localKey = new Project.NameKey("local");
+  private ProjectConfig local;
+  private Util util;
 
   @Override
   public void setUp() throws Exception {
     super.setUp();
-    projects = new Projects(new LabelTypes(Arrays.asList(
-        category("Code-Review",
-            value(2, "Looks good to me, approved"),
-            value(1, "Looks good to me, but someone else must approve"),
-            value(0, "No score"),
-            value(-1, "I would prefer that you didn't submit this"),
-            value(-2, "Do not submit")),
-        category("Verified", value(1, "Verified"),
-            value(0, "No score"), value(-1, "Fails")))));
+    util = new Util();
     load("gerrit", "gerrit_common_test.pl", new AbstractModule() {
       @Override
       protected void configure() {
         bind(PrologEnvironment.Args.class).toInstance(
-          new PrologEnvironment.Args(
-              projects,
-              null,
-              null,
-              null,
-              null,
-              null
-            ));
+            new PrologEnvironment.Args(
+                null,
+                null,
+                null,
+                null,
+                null,
+                null));
       }
     });
+
+    local = new ProjectConfig(localKey);
+    local.createInMemory();
+    Q.setRefPatterns(Arrays.asList("refs/heads/develop"));
+
+    local.getLabelSections().put(V.getName(), V);
+    local.getLabelSections().put(Q.getName(), Q);
+    util.add(local);
+    grant(local, LABEL + V.getName(), -1, +1, REGISTERED, "refs/heads/*");
+    grant(local, LABEL + Q.getName(), -1, +1, REGISTERED, "refs/heads/master");
   }
 
   @Override
   protected void setUpEnvironment(PrologEnvironment env) {
-    env.set(StoredValues.CHANGE, new Change(
-        new Change.Key("Ibeef"), new Change.Id(1), new Account.Id(2),
-        new Branch.NameKey(projects.allProjectsName, "master")));
+    Change change =
+        new Change(new Change.Key("Ibeef"), new Change.Id(1),
+            new Account.Id(2),
+            new Branch.NameKey(localKey, "refs/heads/master"));
+    env.set(StoredValues.CHANGE, change);
+    env.set(StoredValues.CHANGE_CONTROL, util.user(local).controlFor(change));
   }
 
   public void testGerritCommon() {
     runPrologBasedTests();
   }
-
-  private static LabelValue value(int value, String text) {
-    return new LabelValue((short) value, text);
-  }
-
-  private static LabelType category(String name, LabelValue... values) {
-    return new LabelType(name, Arrays.asList(values));
-  }
-
-  private static class Projects implements ProjectCache {
-    private final AllProjectsName allProjectsName;
-    private final ProjectState allProjects;
-
-    private Projects(LabelTypes labelTypes) {
-      allProjectsName = new AllProjectsName("All-Projects");
-      ProjectConfig config = new ProjectConfig(allProjectsName);
-      config.createInMemory();
-      for (LabelType label : labelTypes.getLabelTypes()) {
-        config.getLabelSections().put(label.getName(), label);
-      }
-      allProjects = new ProjectState(null, this, allProjectsName, null, null,
-          null, null, null, config);
-    }
-
-    @Override
-    public ProjectState getAllProjects() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public ProjectState get(Project.NameKey projectName) {
-      assertEquals(allProjectsName, projectName);
-      return allProjects;
-    }
-
-    @Override
-    public ProjectState checkedGet(Project.NameKey projectName) {
-      return get(projectName);
-    }
-
-    @Override
-    public void evict(Project p) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void evict(Project.NameKey p) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void remove(Project p) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Iterable<Project.NameKey> all() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public Iterable<Project.NameKey> byName(String prefix) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void onCreateProject(Project.NameKey newProjectName) {
-      throw new UnsupportedOperationException();
-    }
-  }
 }
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
new file mode 100644
index 0000000..88d6b85
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
@@ -0,0 +1,109 @@
+// 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.git;
+
+package com.google.gerrit.server.index;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+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.ChangeDataSource;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+class FakeIndex implements ChangeIndex {
+  static Schema<ChangeData> V1 = new Schema<ChangeData>(1, false,
+    ImmutableList.<FieldDef<ChangeData, ?>> of(
+      ChangeField.STATUS));
+
+  static Schema<ChangeData> V2 = new Schema<ChangeData>(2, false,
+    ImmutableList.of(
+      ChangeField.STATUS,
+      ChangeField.FILE));
+
+  private static class Source implements ChangeDataSource {
+    private final Predicate<ChangeData> p;
+
+    Source(Predicate<ChangeData> p) {
+      this.p = p;
+    }
+
+    @Override
+    public int getCardinality() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasChange() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ResultSet<ChangeData> read() throws OrmException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String toString() {
+      return p.toString();
+    }
+  }
+
+  private final Schema<ChangeData> schema;
+
+  FakeIndex(Schema<ChangeData> schema) {
+    this.schema = schema;
+  }
+
+  @Override
+  public ListenableFuture<Void> insert(ChangeData cd) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ListenableFuture<Void> replace(ChangeData cd) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ListenableFuture<Void> delete(ChangeData cd) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void deleteAll() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
+      throws QueryParseException {
+    return new FakeIndex.Source(p);
+  }
+
+  @Override
+  public Schema<ChangeData> getSchema() {
+    return schema;
+  }
+
+  @Override
+  public void close() {
+  }
+
+  @Override
+  public void markReady(boolean ready) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
new file mode 100644
index 0000000..f9ef2dd
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeQueryBuilder.java
@@ -0,0 +1,56 @@
+// 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.git;
+
+package com.google.gerrit.server.index;
+
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gwtorm.server.OrmException;
+
+public class FakeQueryBuilder extends ChangeQueryBuilder {
+  FakeQueryBuilder(IndexCollection indexes) {
+    super(
+        new FakeQueryBuilder.Definition<ChangeData, FakeQueryBuilder>(
+          FakeQueryBuilder.class),
+        new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
+          null, null, null, null, null, indexes),
+        null);
+  }
+
+  @Operator
+  public Predicate<ChangeData> foo(String value) {
+    return predicate("foo", value);
+  }
+
+  @Operator
+  public Predicate<ChangeData> bar(String value) {
+    return predicate("bar", value);
+  }
+
+  private Predicate<ChangeData> predicate(String name, String value) {
+    return new OperatorPredicate<ChangeData>(name, value) {
+      @Override
+      public boolean match(ChangeData object) throws OrmException {
+        return false;
+      }
+
+      @Override
+      public int getCost() {
+        return 0;
+      }
+    };
+  }
+}
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 bc96b44..08617b6 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
@@ -21,21 +21,16 @@
 import static com.google.gerrit.reviewdb.client.Change.Status.SUBMITTED;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.util.concurrent.ListenableFuture;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.AndPredicate;
-import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.RewritePredicate;
 import com.google.gerrit.server.query.change.AndSource;
 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.OrSource;
 import com.google.gerrit.server.query.change.SqlRewriterImpl;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 
 import junit.framework.TestCase;
 
@@ -44,127 +39,7 @@
 
 @SuppressWarnings("unchecked")
 public class IndexRewriteTest extends TestCase {
-  private static Schema<ChangeData> V1 = new Schema<ChangeData>(
-      1, false, ImmutableList.<FieldDef<ChangeData, ?>> of(
-          ChangeField.STATUS));
-
-  private static Schema<ChangeData> V2 = new Schema<ChangeData>(
-      2, false, ImmutableList.of(
-          ChangeField.STATUS,
-          ChangeField.FILE));
-
-  private static class DummyIndex implements ChangeIndex {
-    private final Schema<ChangeData> schema;
-
-    private DummyIndex(Schema<ChangeData> schema) {
-      this.schema = schema;
-    }
-
-    @Override
-    public ListenableFuture<Void> insert(ChangeData cd) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public ListenableFuture<Void> replace(ChangeData cd) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public ListenableFuture<Void> delete(ChangeData cd) {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public void deleteAll() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public ChangeDataSource getSource(Predicate<ChangeData> p, int limit)
-        throws QueryParseException {
-      return new Source(p);
-    }
-
-    @Override
-    public Schema<ChangeData> getSchema() {
-      return schema;
-    }
-
-    @Override
-    public void close() {
-    }
-
-    @Override
-    public void markReady(boolean ready) {
-      throw new UnsupportedOperationException();
-    }
-  }
-
-  private static class Source implements ChangeDataSource {
-    private final Predicate<ChangeData> p;
-
-    Source(Predicate<ChangeData> p) {
-      this.p = p;
-    }
-
-    @Override
-    public int getCardinality() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public boolean hasChange() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public ResultSet<ChangeData> read() throws OrmException {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public String toString() {
-      return p.toString();
-    }
-  }
-
-  public class QueryBuilder extends ChangeQueryBuilder {
-    QueryBuilder() {
-      super(
-          new QueryBuilder.Definition<ChangeData, QueryBuilder>(
-            QueryBuilder.class),
-          new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
-            null, null, null, null, null, indexes),
-          null);
-    }
-
-    @Operator
-    public Predicate<ChangeData> foo(String value) {
-      return predicate("foo", value);
-    }
-
-    @Operator
-    public Predicate<ChangeData> bar(String value) {
-      return predicate("bar", value);
-    }
-
-    private Predicate<ChangeData> predicate(String name, String value) {
-      return new OperatorPredicate<ChangeData>(name, value) {
-        @Override
-        public boolean match(ChangeData object) throws OrmException {
-          return false;
-        }
-
-        @Override
-        public int getCost() {
-          return 0;
-        }
-      };
-    }
-  }
-
-  private DummyIndex index;
+  private FakeIndex index;
   private IndexCollection indexes;
   private ChangeQueryBuilder queryBuilder;
   private IndexRewriteImpl rewrite;
@@ -172,10 +47,10 @@
   @Override
   public void setUp() throws Exception {
     super.setUp();
-    index = new DummyIndex(V2);
+    index = new FakeIndex(FakeIndex.V2);
     indexes = new IndexCollection();
     indexes.setSearchIndex(index);
-    queryBuilder = new QueryBuilder();
+    queryBuilder = new FakeQueryBuilder(indexes);
     rewrite = new IndexRewriteImpl(
         indexes,
         null,
@@ -302,7 +177,7 @@
     Predicate<ChangeData> in = parse("status:merged file:a");
     assertEquals(query(in), rewrite(in));
 
-    indexes.setSearchIndex(new DummyIndex(V1));
+    indexes.setSearchIndex(new FakeIndex(FakeIndex.V1));
     Predicate<ChangeData> out = rewrite(in);
     assertTrue(out instanceof AndPredicate);
     assertEquals(ImmutableList.of(
@@ -339,7 +214,7 @@
 
   private IndexedChangeQuery query(Predicate<ChangeData> p, int limit)
       throws QueryParseException {
-    return new IndexedChangeQuery(index, p, limit);
+    return new IndexedChangeQuery(null, index, p, limit);
   }
 
   private Set<Change.Status> status(String query) throws QueryParseException {
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
new file mode 100644
index 0000000..ba1f53d
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexedChangeQueryTest.java
@@ -0,0 +1,67 @@
+// 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.git;
+
+package com.google.gerrit.server.index;
+
+import static com.google.gerrit.server.index.IndexedChangeQuery.replaceSortKeyPredicates;
+
+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 junit.framework.TestCase;
+
+public class IndexedChangeQueryTest extends TestCase {
+  private FakeIndex index;
+  private ChangeQueryBuilder queryBuilder;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    index = new FakeIndex(FakeIndex.V2);
+    IndexCollection indexes = new IndexCollection();
+    indexes.setSearchIndex(index);
+    queryBuilder = new FakeQueryBuilder(indexes);
+  }
+
+  public void testReplaceSortKeyPredicate_NoSortKey() throws Exception {
+    Predicate<ChangeData> p = parse("foo:a bar:b OR (foo:b bar:a)");
+    assertSame(p, replaceSortKeyPredicates(p, "1234"));
+  }
+
+  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"));
+  }
+
+  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/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 968d661..8f4e058 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -20,59 +20,63 @@
 import static com.google.gerrit.common.data.Permission.PUSH;
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.common.data.Permission.SUBMIT;
+import static com.google.gerrit.server.project.Util.ANONYMOUS;
+import static com.google.gerrit.server.project.Util.REGISTERED;
+import static com.google.gerrit.server.project.Util.ADMIN;
+import static com.google.gerrit.server.project.Util.DEVS;
+import static com.google.gerrit.server.project.Util.grant;
+import static com.google.gerrit.server.project.Util.doNotInherit;
 
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.Capable;
-import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.rules.PrologEnvironment;
-import com.google.gerrit.rules.RulesCache;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.CapabilityControl;
-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.FactoryModule;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
 
 import junit.framework.TestCase;
 
-import org.eclipse.jgit.lib.Config;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
 public class RefControlTest extends TestCase {
-  public void testOwnerProject() {
-    grant(local, OWNER, admin, "refs/*");
+  private static void assertOwner(String ref, ProjectControl u) {
+    assertTrue("OWN " + ref, u.controlForRef(ref).isOwner());
+  }
 
-    ProjectControl uBlah = user(devs);
-    ProjectControl uAdmin = user(devs, admin);
+  private static void assertNotOwner(String ref, ProjectControl u) {
+    assertFalse("NOT OWN " + ref, u.controlForRef(ref).isOwner());
+  }
+
+  private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
+  private Project.NameKey localKey = new Project.NameKey("local");
+  private ProjectConfig local;
+  private final Util util;
+
+  public RefControlTest() {
+    util = new Util();
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    local = new ProjectConfig(localKey);
+    local.createInMemory();
+    util.add(local);
+  }
+
+  public void testOwnerProject() {
+    grant(local, OWNER, ADMIN, "refs/*");
+
+    ProjectControl uBlah = util.user(local, DEVS);
+    ProjectControl uAdmin = util.user(local, DEVS, ADMIN);
 
     assertFalse("not owner", uBlah.isOwner());
     assertTrue("is owner", uAdmin.isOwner());
   }
 
   public void testBranchDelegation1() {
-    grant(local, OWNER, admin, "refs/*");
-    grant(local, OWNER, devs, "refs/heads/x/*");
+    grant(local, OWNER, ADMIN, "refs/*");
+    grant(local, OWNER, DEVS, "refs/heads/x/*");
 
-    ProjectControl uDev = user(devs);
+    ProjectControl uDev = util.user(local, DEVS);
     assertFalse("not owner", uDev.isOwner());
     assertTrue("owns ref", uDev.isOwnerAnyRef());
 
@@ -85,12 +89,12 @@
   }
 
   public void testBranchDelegation2() {
-    grant(local, OWNER, admin, "refs/*");
-    grant(local, OWNER, devs, "refs/heads/x/*");
+    grant(local, OWNER, ADMIN, "refs/*");
+    grant(local, OWNER, DEVS, "refs/heads/x/*");
     grant(local, OWNER, fixers, "refs/heads/x/y/*");
     doNotInherit(local, OWNER, "refs/heads/x/y/*");
 
-    ProjectControl uDev = user(devs);
+    ProjectControl uDev = util.user(local, DEVS);
     assertFalse("not owner", uDev.isOwner());
     assertTrue("owns ref", uDev.isOwnerAnyRef());
 
@@ -100,7 +104,7 @@
     assertNotOwner("refs/*", uDev);
     assertNotOwner("refs/heads/master", uDev);
 
-    ProjectControl uFix = user(fixers);
+    ProjectControl uFix = util.user(local, fixers);
     assertFalse("not owner", uFix.isOwner());
     assertTrue("owns ref", uFix.isOwnerAnyRef());
 
@@ -113,13 +117,13 @@
   }
 
   public void testInheritRead_SingleBranchDeniesUpload() {
-    grant(parent, READ, registered, "refs/*");
-    grant(parent, PUSH, registered, "refs/for/refs/*");
-    grant(local, READ, registered, "refs/heads/foobar");
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(util.getParentConfig(), PUSH, REGISTERED, "refs/for/refs/*");
+    grant(local, READ, REGISTERED, "refs/heads/foobar");
     doNotInherit(local, READ, "refs/heads/foobar");
     doNotInherit(local, PUSH, "refs/for/refs/heads/foobar");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
 
     assertTrue("can upload refs/heads/master", //
@@ -130,11 +134,11 @@
   }
 
   public void testInheritRead_SingleBranchDoesNotOverrideInherited() {
-    grant(parent, READ, registered, "refs/*");
-    grant(parent, PUSH, registered, "refs/for/refs/*");
-    grant(local, READ, registered, "refs/heads/foobar");
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(util.getParentConfig(), PUSH, REGISTERED, "refs/for/refs/*");
+    grant(local, READ, REGISTERED, "refs/heads/foobar");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
 
     assertTrue("can upload refs/heads/master", //
@@ -145,30 +149,30 @@
   }
 
   public void testInheritDuplicateSections() {
-    grant(parent, READ, admin, "refs/*");
-    grant(local, READ, devs, "refs/heads/*");
-    local.getProject().setParentName(parent.getProject().getName());
-    assertTrue("a can read", user("a", admin).isVisible());
+    grant(util.getParentConfig(), READ, ADMIN, "refs/*");
+    grant(local, READ, DEVS, "refs/heads/*");
+    local.getProject().setParentName(util.getParentConfig().getProject().getName());
+    assertTrue("a can read", util.user(local, "a", ADMIN).isVisible());
 
     local = new ProjectConfig(new Project.NameKey("local"));
     local.createInMemory();
-    grant(local, READ, devs, "refs/*");
-    assertTrue("d can read", user("d", devs).isVisible());
+    grant(local, READ, DEVS, "refs/*");
+    assertTrue("d can read", util.user(local, "d", DEVS).isVisible());
   }
 
   public void testInheritRead_OverrideWithDeny() {
-    grant(parent, READ, registered, "refs/*");
-    grant(local, READ, registered, "refs/*").setDeny();
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(local, READ, REGISTERED, "refs/*").setDeny();
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertFalse("can't read", u.isVisible());
   }
 
   public void testInheritRead_AppendWithDenyOfRef() {
-    grant(parent, READ, registered, "refs/*");
-    grant(local, READ, registered, "refs/heads/*").setDeny();
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(local, READ, REGISTERED, "refs/heads/*").setDeny();
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertTrue("can read", u.isVisible());
     assertTrue("can read", u.controlForRef("refs/master").isVisible());
     assertTrue("can read", u.controlForRef("refs/tags/foobar").isVisible());
@@ -176,11 +180,11 @@
   }
 
   public void testInheritRead_OverridesAndDeniesOfRef() {
-    grant(parent, READ, registered, "refs/*");
-    grant(local, READ, registered, "refs/*").setDeny();
-    grant(local, READ, registered, "refs/heads/*");
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(local, READ, REGISTERED, "refs/*").setDeny();
+    grant(local, READ, REGISTERED, "refs/heads/*");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertTrue("can read", u.isVisible());
     assertFalse("can't read", u.controlForRef("refs/foobar").isVisible());
     assertFalse("can't read", u.controlForRef("refs/tags/foobar").isVisible());
@@ -188,65 +192,65 @@
   }
 
   public void testInheritSubmit_OverridesAndDeniesOfRef() {
-    grant(parent, SUBMIT, registered, "refs/*");
-    grant(local, SUBMIT, registered, "refs/*").setDeny();
-    grant(local, SUBMIT, registered, "refs/heads/*");
+    grant(util.getParentConfig(), SUBMIT, REGISTERED, "refs/*");
+    grant(local, SUBMIT, REGISTERED, "refs/*").setDeny();
+    grant(local, SUBMIT, REGISTERED, "refs/heads/*");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertFalse("can't submit", u.controlForRef("refs/foobar").canSubmit());
     assertFalse("can't submit", u.controlForRef("refs/tags/foobar").canSubmit());
     assertTrue("can submit", u.controlForRef("refs/heads/foobar").canSubmit());
   }
 
   public void testCannotUploadToAnyRef() {
-    grant(parent, READ, registered, "refs/*");
-    grant(local, READ, devs, "refs/heads/*");
-    grant(local, PUSH, devs, "refs/for/refs/heads/*");
+    grant(util.getParentConfig(), READ, REGISTERED, "refs/*");
+    grant(local, READ, DEVS, "refs/heads/*");
+    grant(local, PUSH, DEVS, "refs/for/refs/heads/*");
 
-    ProjectControl u = user();
+    ProjectControl u = util.user(local);
     assertFalse("cannot upload", u.canPushToAtLeastOneRef() == Capable.OK);
     assertFalse("cannot upload refs/heads/master", //
         u.controlForRef("refs/heads/master").canUpload());
   }
 
   public void testUsernamePatternNonRegex() {
-    grant(local, READ, devs, "refs/sb/${username}/heads/*");
+    grant(local, READ, DEVS, "refs/sb/${username}/heads/*");
 
-    ProjectControl u = user("u", devs), d = user("d", devs);
+    ProjectControl u = util.user(local, "u", DEVS), d = util.user(local, "d", DEVS);
     assertFalse("u can't read", u.controlForRef("refs/sb/d/heads/foobar").isVisible());
     assertTrue("d can read", d.controlForRef("refs/sb/d/heads/foobar").isVisible());
   }
 
   public void testUsernamePatternWithRegex() {
-    grant(local, READ, devs, "^refs/sb/${username}/heads/.*");
+    grant(local, READ, DEVS, "^refs/sb/${username}/heads/.*");
 
-    ProjectControl u = user("d.v", devs), d = user("dev", devs);
+    ProjectControl u = util.user(local, "d.v", DEVS), d = util.user(local, "dev", DEVS);
     assertFalse("u can't read", u.controlForRef("refs/sb/dev/heads/foobar").isVisible());
     assertTrue("d can read", d.controlForRef("refs/sb/dev/heads/foobar").isVisible());
   }
 
   public void testSortWithRegex() {
-    grant(local, READ, devs, "^refs/heads/.*");
-    grant(parent, READ, anonymous, "^refs/heads/.*-QA-.*");
+    grant(local, READ, DEVS, "^refs/heads/.*");
+    grant(util.getParentConfig(), READ, ANONYMOUS, "^refs/heads/.*-QA-.*");
 
-    ProjectControl u = user(devs), d = user(devs);
+    ProjectControl u = util.user(local, DEVS), d = util.user(local, DEVS);
     assertTrue("u can read", u.controlForRef("refs/heads/foo-QA-bar").isVisible());
     assertTrue("d can read", d.controlForRef("refs/heads/foo-QA-bar").isVisible());
   }
 
   public void testBlockRule_ParentBlocksChild() {
-    grant(local, PUSH, devs, "refs/tags/*");
-    grant(parent, PUSH, anonymous, "refs/tags/*").setBlock();
+    grant(local, PUSH, DEVS, "refs/tags/*");
+    grant(util.getParentConfig(), PUSH, ANONYMOUS, "refs/tags/*").setBlock();
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't force update tag", u.controlForRef("refs/tags/V10").canForceUpdate());
   }
 
   public void testBlockLabelRange_ParentBlocksChild() {
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
-    grant(parent, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
+    grant(util.getParentConfig(), LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*").setBlock();
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
 
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertTrue("u can vote -1", range.contains(-1));
@@ -256,338 +260,140 @@
   }
 
   public void testUnblockNoForce() {
-    grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
-    grant(local, PUSH, devs, "refs/heads/*");
+    grant(local, PUSH, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, PUSH, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertTrue("u can push", u.controlForRef("refs/heads/master").canUpdate());
   }
 
   public void testUnblockForce() {
-    PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
+    PermissionRule r = grant(local, PUSH, ANONYMOUS, "refs/heads/*");
     r.setBlock();
     r.setForce(true);
-    grant(local, PUSH, devs, "refs/heads/*").setForce(true);
+    grant(local, PUSH, DEVS, "refs/heads/*").setForce(true);
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertTrue("u can force push", u.controlForRef("refs/heads/master").canForceUpdate());
   }
 
   public void testUnblockForceWithAllowNoForce_NotPossible() {
-    PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
+    PermissionRule r = grant(local, PUSH, ANONYMOUS, "refs/heads/*");
     r.setBlock();
     r.setForce(true);
-    grant(local, PUSH, devs, "refs/heads/*");
+    grant(local, PUSH, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't force push", u.controlForRef("refs/heads/master").canForceUpdate());
   }
 
   public void testUnblockMoreSpecificRef_Fails() {
-    grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
-    grant(local, PUSH, devs, "refs/heads/master");
+    grant(local, PUSH, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, PUSH, DEVS, "refs/heads/master");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
   }
 
   public void testUnblockLargerScope_Fails() {
-    grant(local, PUSH, anonymous, "refs/heads/master").setBlock();
-    grant(local, PUSH, devs, "refs/heads/*");
+    grant(local, PUSH, ANONYMOUS, "refs/heads/master").setBlock();
+    grant(local, PUSH, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
   }
 
   public void testUnblockInLocal_Fails() {
-    grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(util.getParentConfig(), PUSH, ANONYMOUS, "refs/heads/*").setBlock();
     grant(local, PUSH, fixers, "refs/heads/*");
 
-    ProjectControl f = user(fixers);
+    ProjectControl f = util.user(local, fixers);
     assertFalse("u can't push", f.controlForRef("refs/heads/master").canUpdate());
   }
 
   public void testUnblockInParentBlockInLocal() {
-    grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
-    grant(parent, PUSH, devs, "refs/heads/*");
-    grant(local, PUSH, devs, "refs/heads/*").setBlock();
+    grant(util.getParentConfig(), PUSH, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(util.getParentConfig(), PUSH, DEVS, "refs/heads/*");
+    grant(local, PUSH, DEVS, "refs/heads/*").setBlock();
 
-    ProjectControl d = user(devs);
+    ProjectControl d = util.user(local, DEVS);
     assertFalse("u can't push", d.controlForRef("refs/heads/master").canUpdate());
   }
 
-  public void testUnblockVisibilityByRegisteredUsers() {
-    grant(local, READ, anonymous, "refs/heads/*").setBlock();
-    grant(local, READ, registered, "refs/heads/*");
+  public void testUnblockVisibilityByREGISTEREDUsers() {
+    grant(local, READ, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, READ, REGISTERED, "refs/heads/*");
 
-    ProjectControl u = user(registered);
+    ProjectControl u = util.user(local, REGISTERED);
     assertTrue("u can read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
   }
 
   public void testUnblockInLocalVisibilityByRegisteredUsers_Fails() {
-    grant(parent, READ, anonymous, "refs/heads/*").setBlock();
-    grant(local, READ, registered, "refs/heads/*");
+    grant(util.getParentConfig(), READ, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, READ, REGISTERED, "refs/heads/*");
 
-    ProjectControl u = user(registered);
+    ProjectControl u = util.user(local, REGISTERED);
     assertFalse("u can't read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
   }
 
   public void testUnblockForceEditTopicName() {
-    grant(local, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
-    grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
+    grant(local, EDIT_TOPIC_NAME, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
 
-    ProjectControl u = user(devs);
-    assertTrue("u can edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
+    ProjectControl u = util.user(local, DEVS);
+    assertTrue("u can edit topic name", u.controlForRef("refs/heads/master")
+        .canForceEditTopicName());
   }
 
   public void testUnblockInLocalForceEditTopicName_Fails() {
-    grant(parent, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
-    grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
+    grant(util.getParentConfig(), EDIT_TOPIC_NAME, ANONYMOUS, "refs/heads/*")
+        .setBlock();
+    grant(local, EDIT_TOPIC_NAME, DEVS, "refs/heads/*").setForce(true);
 
-    ProjectControl u = user(registered);
-    assertFalse("u can't edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
+    ProjectControl u = util.user(local, REGISTERED);
+    assertFalse("u can't edit topic name", u.controlForRef("refs/heads/master")
+        .canForceEditTopicName());
   }
 
   public void testUnblockRange() {
-    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+    grant(local, LABEL + "Code-Review", -1, +1, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertTrue("u can vote -2", range.contains(-2));
     assertTrue("u can vote +2", range.contains(2));
   }
 
   public void testUnblockRangeOnMoreSpecificRef_Fails() {
-    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/master");
+    grant(local, LABEL + "Code-Review", -1, +1, ANONYMOUS, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/master");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertFalse("u can't vote -2", range.contains(-2));
     assertFalse("u can't vote +2", range.contains(-2));
   }
 
   public void testUnblockRangeOnLargerScope_Fails() {
-    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/master").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+    grant(local, LABEL + "Code-Review", -1, +1, ANONYMOUS, "refs/heads/master").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
+    ProjectControl u = util.user(local, DEVS);
     PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertFalse("u can't vote -2", range.contains(-2));
     assertFalse("u can't vote +2", range.contains(-2));
   }
 
   public void testUnblockInLocalRange_Fails() {
-    grant(parent, LABEL + "Code-Review", -1, 1, anonymous, "refs/heads/*").setBlock();
-    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+    grant(util.getParentConfig(), LABEL + "Code-Review", -1, 1, ANONYMOUS,
+        "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, DEVS, "refs/heads/*");
 
-    ProjectControl u = user(devs);
-    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    ProjectControl u = util.user(local, DEVS);
+    PermissionRange range =
+        u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
     assertFalse("u can't vote -2", range.contains(-2));
     assertFalse("u can't vote 2", range.contains(2));
   }
-  // -----------------------------------------------------------------------
-
-  private final Map<Project.NameKey, ProjectState> all;
-  private final AllProjectsName allProjectsName = new AllProjectsName("parent");
-  private final ProjectCache projectCache;
-
-  private ProjectConfig local;
-  private ProjectConfig parent;
-  private PermissionCollection.Factory sectionSorter;
-
-  private final AccountGroup.UUID admin = new AccountGroup.UUID("test.admin");
-  private final AccountGroup.UUID anonymous = AccountGroup.ANONYMOUS_USERS;
-  private final AccountGroup.UUID registered = AccountGroup.REGISTERED_USERS;
-
-  private final AccountGroup.UUID devs = new AccountGroup.UUID("test.devs");
-  private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
-
-  private final CapabilityControl.Factory capabilityControlFactory;
-
-  public RefControlTest() {
-    all = new HashMap<Project.NameKey, ProjectState>();
-    projectCache = new ProjectCache() {
-      @Override
-      public ProjectState getAllProjects() {
-        return get(allProjectsName);
-      }
-
-      @Override
-      public ProjectState get(Project.NameKey projectName) {
-        return all.get(projectName);
-      }
-
-      @Override
-      public ProjectState checkedGet(Project.NameKey projectName) {
-        return get(projectName);
-      }
-
-      @Override
-      public void evict(Project p) {
-      }
-
-      @Override
-      public void evict(Project.NameKey p) {
-      }
-
-      @Override
-      public void remove(Project p) {
-      }
-
-      @Override
-      public Iterable<Project.NameKey> all() {
-        return Collections.emptySet();
-      }
-
-      @Override
-      public Iterable<Project.NameKey> byName(String prefix) {
-        return Collections.emptySet();
-      }
-
-      @Override
-      public void onCreateProject(Project.NameKey newProjectName) {
-      }
-
-      @Override
-      public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
-        return Collections.emptySet();
-      }
-    };
-
-    Injector injector = Guice.createInjector(new FactoryModule() {
-      @Override
-      protected void configure() {
-        bind(Config.class)
-            .annotatedWith(GerritServerConfig.class)
-            .toInstance(new Config());
-
-        factory(CapabilityControl.Factory.class);
-        bind(ProjectCache.class).toInstance(projectCache);
-      }
-    });
-    capabilityControlFactory = injector.getInstance(CapabilityControl.Factory.class);
-  }
-
-  @Override
-  public void setUp() throws Exception {
-    super.setUp();
-
-    parent = new ProjectConfig(new Project.NameKey("parent"));
-    parent.createInMemory();
-
-    local = new ProjectConfig(new Project.NameKey("local"));
-    local.createInMemory();
-
-    Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c =
-        CacheBuilder.newBuilder().build();
-    sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c));
-  }
-
-  private static void assertOwner(String ref, ProjectControl u) {
-    assertTrue("OWN " + ref, u.controlForRef(ref).isOwner());
-  }
-
-  private static void assertNotOwner(String ref, ProjectControl u) {
-    assertFalse("NOT OWN " + ref, u.controlForRef(ref).isOwner());
-  }
-
-  private PermissionRule grant(ProjectConfig project, String permissionName,
-      AccountGroup.UUID group, String ref) {
-    return grant(project, permissionName, newRule(project, group), ref);
-  }
-
-  private PermissionRule grant(ProjectConfig project, String permissionName,
-      int min, int max, AccountGroup.UUID group, String ref) {
-    PermissionRule rule = newRule(project, group);
-    rule.setMin(min);
-    rule.setMax(max);
-    return grant(project, permissionName, rule, ref);
-  }
-
-
-  private PermissionRule grant(ProjectConfig project, String permissionName,
-      PermissionRule rule, String ref) {
-    project.getAccessSection(ref, true) //
-        .getPermission(permissionName, true) //
-        .add(rule);
-    return rule;
-  }
-
-  private void doNotInherit(ProjectConfig project, String permissionName,
-      String ref) {
-    project.getAccessSection(ref, true) //
-        .getPermission(permissionName, true) //
-        .setExclusiveGroup(true);
-  }
-
-  private PermissionRule newRule(ProjectConfig project, AccountGroup.UUID groupUUID) {
-    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
-    group = project.resolve(group);
-
-    return new PermissionRule(group);
-  }
-
-  private ProjectControl user(AccountGroup.UUID... memberOf) {
-    return user(null, memberOf);
-  }
-
-  private ProjectControl user(String name, AccountGroup.UUID... memberOf) {
-    String canonicalWebUrl = "http://localhost";
-
-    return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
-        Collections.<AccountGroup.UUID> emptySet(), projectCache,
-        sectionSorter,
-        canonicalWebUrl, new MockUser(name, memberOf),
-        newProjectState());
-  }
-
-  private ProjectState newProjectState() {
-    PrologEnvironment.Factory envFactory = null;
-    GitRepositoryManager mgr = null;
-    ProjectControl.AssistedFactory projectControlFactory = null;
-    RulesCache rulesCache = null;
-    all.put(local.getProject().getNameKey(), new ProjectState(
-        null, projectCache, allProjectsName, projectControlFactory,
-        envFactory, mgr, rulesCache, null, local));
-    all.put(parent.getProject().getNameKey(), new ProjectState(
-        null, projectCache, allProjectsName, projectControlFactory,
-        envFactory, mgr, rulesCache, null, parent));
-    return all.get(local.getProject().getNameKey());
-  }
-
-  private class MockUser extends CurrentUser {
-    private final String username;
-    private final GroupMembership groups;
-
-    MockUser(String name, AccountGroup.UUID[] groupId) {
-      super(RefControlTest.this.capabilityControlFactory);
-      username = name;
-      ArrayList<AccountGroup.UUID> groupIds = Lists.newArrayList(groupId);
-      groupIds.add(registered);
-      groupIds.add(anonymous);
-      groups = new ListGroupMembership(groupIds);
-    }
-
-    @Override
-    public GroupMembership getEffectiveGroups() {
-      return groups;
-    }
-
-    @Override
-    public String getUserName() {
-      return username;
-    }
-
-    @Override
-    public Set<Change.Id> getStarredChanges() {
-      return Collections.emptySet();
-    }
-
-    @Override
-    public Collection<AccountProjectWatch> getNotificationFilters() {
-      return Collections.emptySet();
-    }
-  }
 }
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
new file mode 100644
index 0000000..07f903c
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
@@ -0,0 +1,264 @@
+// 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.project;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+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.rules.PrologEnvironment;
+import com.google.gerrit.rules.RulesCache;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
+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.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.inject.Guice;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class Util {
+  public static AccountGroup.UUID ANONYMOUS = AccountGroup.ANONYMOUS_USERS;
+  public static AccountGroup.UUID REGISTERED = AccountGroup.REGISTERED_USERS;
+  public static AccountGroup.UUID ADMIN = new AccountGroup.UUID("test.admin");
+  public static AccountGroup.UUID DEVS = new AccountGroup.UUID("test.devs");
+
+  public static LabelType CR = category("Code-Review",
+      value(2, "Looks good to me, approved"),
+      value(1, "Looks good to me, but someone else must approve"),
+      value(0, "No score"),
+      value(-1, "I would prefer that you didn't submit this"),
+      value(-2, "Do not submit"));
+
+  public static LabelValue value(int value, String text) {
+    return new LabelValue((short) value, text);
+  }
+
+  public static LabelType category(String name, LabelValue... values) {
+    return new LabelType(name, Arrays.asList(values));
+  }
+
+  static public PermissionRule newRule(ProjectConfig project,
+      AccountGroup.UUID groupUUID) {
+    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
+    group = project.resolve(group);
+
+    return new PermissionRule(group);
+  }
+
+  static public PermissionRule grant(ProjectConfig project,
+      String permissionName, int min, int max, AccountGroup.UUID group,
+      String ref) {
+    PermissionRule rule = newRule(project, group);
+    rule.setMin(min);
+    rule.setMax(max);
+    return grant(project, permissionName, rule, ref);
+  }
+
+  static public PermissionRule grant(ProjectConfig project,
+      String permissionName, AccountGroup.UUID group, String ref) {
+    return grant(project, permissionName, newRule(project, group), ref);
+  }
+
+  static public void doNotInherit(ProjectConfig project, String permissionName,
+      String ref) {
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .setExclusiveGroup(true);
+  }
+
+  static private PermissionRule grant(ProjectConfig project,
+      String permissionName, PermissionRule rule, String ref) {
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .add(rule);
+    return rule;
+  }
+
+  private final Map<Project.NameKey, ProjectState> all;
+  private final ProjectCache projectCache;
+  private final CapabilityControl.Factory capabilityControlFactory;
+  private final PermissionCollection.Factory sectionSorter;
+
+  private final AllProjectsName allProjectsName = new AllProjectsName("parent");
+  private final ProjectConfig parent = new ProjectConfig(allProjectsName);
+
+  public Util() {
+    all = new HashMap<Project.NameKey, ProjectState>();
+    parent.createInMemory();
+    parent.getLabelSections().put(CR.getName(), CR);
+
+    add(parent);
+
+    projectCache = new ProjectCache() {
+      @Override
+      public ProjectState getAllProjects() {
+        return get(allProjectsName);
+      }
+
+      @Override
+      public ProjectState get(Project.NameKey projectName) {
+        return all.get(projectName);
+      }
+
+      @Override
+      public void evict(Project p) {
+      }
+
+      @Override
+      public void remove(Project p) {
+      }
+
+      @Override
+      public Iterable<Project.NameKey> all() {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public Iterable<Project.NameKey> byName(String prefix) {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public void onCreateProject(Project.NameKey newProjectName) {
+      }
+
+      @Override
+      public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public ProjectState checkedGet(NameKey projectName) throws IOException {
+        return all.get(projectName);
+      }
+
+      @Override
+      public void evict(NameKey p) {
+      }
+    };
+
+    Injector injector = Guice.createInjector(new FactoryModule() {
+      @Override
+      protected void configure() {
+        bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(
+            new Config());
+
+        factory(CapabilityControl.Factory.class);
+        bind(ProjectCache.class).toInstance(projectCache);
+      }
+    });
+
+    Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c =
+        CacheBuilder.newBuilder().build();
+    sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c));
+    capabilityControlFactory =
+        injector.getInstance(CapabilityControl.Factory.class);
+  }
+
+  public ProjectConfig getParentConfig() {
+    return this.parent;
+  }
+
+  public void add(ProjectConfig pc) {
+    PrologEnvironment.Factory envFactory = null;
+    GitRepositoryManager mgr = null;
+    ProjectControl.AssistedFactory projectControlFactory = null;
+    RulesCache rulesCache = null;
+    SitePaths sitePaths = null;
+    List<CommentLinkInfo> commentLinks = null;
+
+    all.put(pc.getProject().getNameKey(), new ProjectState(sitePaths,
+        projectCache, allProjectsName, projectControlFactory, envFactory, mgr,
+        rulesCache, commentLinks, pc));
+  }
+
+  public ProjectControl user(ProjectConfig local, AccountGroup.UUID... memberOf) {
+    return user(local, null, memberOf);
+  }
+
+  public ProjectControl user(ProjectConfig local, String name,
+      AccountGroup.UUID... memberOf) {
+    String canonicalWebUrl = "http://localhost";
+
+    return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
+        Collections.<AccountGroup.UUID> emptySet(), projectCache,
+        sectionSorter, canonicalWebUrl, new MockUser(name, memberOf),
+        newProjectState(local));
+  }
+
+  private ProjectState newProjectState(ProjectConfig local) {
+    add(local);
+    return all.get(local.getProject().getNameKey());
+  }
+
+  private class MockUser extends CurrentUser {
+    private final String username;
+    private final GroupMembership groups;
+
+    MockUser(String name, AccountGroup.UUID[] groupId) {
+      super(capabilityControlFactory);
+      username = name;
+      ArrayList<AccountGroup.UUID> groupIds = Lists.newArrayList(groupId);
+      groupIds.add(REGISTERED);
+      groupIds.add(ANONYMOUS);
+      groups = new ListGroupMembership(groupIds);
+    }
+
+    @Override
+    public GroupMembership getEffectiveGroups() {
+      return groups;
+    }
+
+    @Override
+    public String getUserName() {
+      return username;
+    }
+
+    @Override
+    public Set<Change.Id> getStarredChanges() {
+      return Collections.emptySet();
+    }
+
+    @Override
+    public Collection<AccountProjectWatch> getNotificationFilters() {
+      return Collections.emptySet();
+    }
+  }
+}
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 33150e8..2ddbecb 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
@@ -41,6 +41,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 
@@ -204,7 +205,8 @@
     if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
       indexes.add(closedIndex);
     }
-    return new QuerySource(indexes, QueryBuilder.toQuery(p), limit);
+    return new QuerySource(indexes, QueryBuilder.toQuery(schema, p), limit,
+        ChangeQueryBuilder.hasNonTrivialSortKeyAfter(schema, p));
   }
 
   private void commit(SolrServer server) throws IOException {
@@ -219,7 +221,9 @@
     private final List<SolrServer> indexes;
     private final SolrQuery query;
 
-    public QuerySource(List<SolrServer> indexes, Query q, int limit) {
+    @SuppressWarnings("deprecation")
+    public QuerySource(List<SolrServer> indexes, Query q, int limit,
+        boolean reverse) {
       this.indexes = indexes;
 
       query = new SolrQuery(q.toString());
@@ -227,8 +231,8 @@
       query.setParam("rows", Integer.toString(limit));
       query.setFields(ID_FIELD);
       query.setSort(
-          ChangeField.UPDATED.getName(),
-          SolrQuery.ORDER.desc);
+          ChangeField.SORTKEY.getName(),
+          !reverse ? SolrQuery.ORDER.desc : SolrQuery.ORDER.asc);
     }
 
     @Override
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
index 25b9eb8..6a1fdd7 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -2,11 +2,21 @@
 
 java_binary(
   name = 'asciidoc',
-  main_class = 'org.asciidoctor.cli.AsciidoctorInvoker',
-  deps = [':asciidoctor'],
+  main_class = 'Main',
+  deps = [':main_lib'],
   visibility = ['PUBLIC'],
 )
 
+java_library(
+  name = 'main_lib',
+  srcs = ['java/Main.java'],
+  deps = [
+    ':asciidoctor',
+    ':jruby',
+    '//lib:args4j',
+  ],
+)
+
 maven_jar(
   name = 'asciidoctor',
   id = 'org.asciidoctor:asciidoctor-java-integration:0.1.3',
@@ -14,19 +24,6 @@
   license = 'Apache2.0',
   visibility = [],
   attach_source = False,
-  deps = [
-    ':jcommander',
-    ':jruby',
-  ],
-)
-
-maven_jar(
-  name = 'jcommander',
-  id = 'com.beust:jcommander:1.30',
-  sha1 = 'c440b30a944ba199751551aee393f8aa03b3c327',
-  license = 'Apache2.0',
-  visibility = [],
-  attach_source = False,
 )
 
 maven_jar(
diff --git a/lib/asciidoctor/java/Main.java b/lib/asciidoctor/java/Main.java
new file mode 100644
index 0000000..eb1ef33
--- /dev/null
+++ b/lib/asciidoctor/java/Main.java
@@ -0,0 +1,159 @@
+// 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.
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.asciidoctor.Asciidoctor;
+import org.asciidoctor.AttributesBuilder;
+import org.asciidoctor.Options;
+import org.asciidoctor.OptionsBuilder;
+import org.asciidoctor.internal.JRubyAsciidoctor;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+
+public class Main {
+
+  private static final int BUFSIZ = 4096;
+  private static final String DOCTYPE = "article";
+  private static final String ERUBY = "erb";
+
+  @Option(name = "-b", usage = "set output format backend")
+  private String backend = "html5";
+
+  @Option(name = "-z", usage = "output zip file")
+  private String zipFile;
+
+  @Option(name = "--in-ext", usage = "extension for input files")
+  private String inExt = ".txt";
+
+  @Option(name = "--out-ext", usage = "extension for output files")
+  private String outExt = ".html";
+
+  @Option(name = "-a", usage =
+      "a list of attributes, in the form key or key=value pair")
+  private List<String> attributes = new ArrayList<String>();
+
+  @Argument(usage = "input files")
+  private List<String> inputFiles = new ArrayList<String>();
+
+  private String mapInFileToOutFile(String inFile) {
+    String basename = new File(inFile).getName();
+    if (basename.endsWith(inExt)) {
+      basename = basename.substring(0, basename.length() - inExt.length());
+    } else {
+      // Strip out the last extension
+      int pos = basename.lastIndexOf('.');
+      if (pos > 0) {
+        basename = basename.substring(0, pos);
+      }
+    }
+    return basename + outExt;
+  }
+
+  private Options createOptions(File tmpFile) {
+    OptionsBuilder optionsBuilder = OptionsBuilder.options();
+
+    optionsBuilder.backend(backend).docType(DOCTYPE).eruby(ERUBY);
+    // XXX(fishywang): ideally we should just output to a string and add the
+    // content into zip. But asciidoctor will actually ignore all attributes if
+    // not output to a file. So we *have* to output to a file then read the
+    // content of the file into zip.
+    optionsBuilder.toFile(tmpFile);
+
+    AttributesBuilder attributesBuilder = AttributesBuilder.attributes();
+    attributesBuilder.attributes(getAttributes());
+    optionsBuilder.attributes(attributesBuilder.get());
+
+    return optionsBuilder.get();
+  }
+
+  private Map<String, Object> getAttributes() {
+    Map<String, Object> attributeValues = new HashMap<String, Object>();
+
+    for (String attribute : attributes) {
+      int equalsIndex = attribute.indexOf('=');
+      if(equalsIndex > -1) {
+        String name = attribute.substring(0, equalsIndex);
+        String value = attribute.substring(equalsIndex + 1, attribute.length());
+
+        attributeValues.put(name, value);
+      } else {
+        attributeValues.put(attribute, "");
+      }
+    }
+
+    return attributeValues;
+  }
+
+  private void invoke(String... parameters) throws IOException {
+    CmdLineParser parser = new CmdLineParser(this);
+    try {
+      parser.parseArgument(parameters);
+      if (inputFiles.isEmpty()) {
+        throw new CmdLineException(parser,
+            "asciidoctor: FAILED: input file missing");
+      }
+    } catch (CmdLineException e) {
+      System.err.println(e.getMessage());
+      parser.printUsage(System.err);
+      System.exit(1);
+      return;
+    }
+
+    ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile));
+    byte[] buf = new byte[BUFSIZ];
+    for (String inputFile : inputFiles) {
+      File tmp = File.createTempFile("doc", ".html");
+      Options options = createOptions(tmp);
+      renderInput(options, inputFile);
+
+      FileInputStream input = new FileInputStream(tmp);
+      int len;
+      zip.putNextEntry(new ZipEntry(mapInFileToOutFile(inputFile)));
+      while ((len = input.read(buf)) > 0) {
+        zip.write(buf, 0, len);
+      }
+      input.close();
+      tmp.delete();
+      zip.closeEntry();
+    }
+    zip.close();
+  }
+
+  private void renderInput(Options options, String inputFile) {
+    Asciidoctor asciidoctor = JRubyAsciidoctor.create();
+    asciidoctor.renderFile(new File(inputFile), options);
+  }
+
+  public static void main(String[] args) {
+    try {
+      new Main().invoke(args);
+    } catch (IOException e) {
+      System.err.println(e.getMessage());
+      System.exit(1);
+    }
+  }
+}
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index fd066ef..6f412e4 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -73,7 +73,6 @@
   id = 'commons-io:commons-io:1.4',
   sha1 = 'a8762d07e76cfde2395257a5da47ba7c1dbd3dce',
   license = 'Apache2.0',
-  exclude = ['META-INF/MANIFEST.MF'],
 )
 
 maven_jar(
diff --git a/plugins/replication b/plugins/replication
index 0c5fef2..1308973 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 0c5fef2a8eda0e76677e5487d2fb8391e1cb126e
+Subproject commit 13089730a07f21eab4a1a302613f78729dc51e1b
diff --git a/tools/BUCK b/tools/BUCK
index 4323ff9..b03dcbc 100644
--- a/tools/BUCK
+++ b/tools/BUCK
@@ -1,22 +1,3 @@
-genrule(
-  name = 'download',
-  cmd = '$(exe :download_all)',
-  deps = [':download_all'],
-  out = '__fake.download__',
-)
-
-genrule(
-  name = 'download_sources',
-  cmd = '$(exe :download_all) --src',
-  deps = [':download_all'],
-  out = '__fake.download__',
-)
-
-python_binary(
-  name = 'download_all',
-  main = 'download_all.py',
-)
-
 python_binary(
   name = 'download_file',
   main = 'download_file.py',
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
index 4e752e9..874c3fc 100644
--- a/tools/eclipse/BUCK
+++ b/tools/eclipse/BUCK
@@ -1,27 +1,5 @@
 include_defs('//tools/build.defs')
 
-genrule(
-  name = 'eclipse',
-  cmd = '',
-  deps = [
-    ':_classpath',
-    ':_project',
-    '//tools:buck.properties',
-  ],
-  out = '__fake.eclipse__',
-)
-
-genrule(
-  name = 'eclipse_project',
-  cmd = '',
-  deps = [
-    ':_classpath_nocompile',
-    ':_project',
-    '//tools:buck.properties',
-  ],
-  out = '__fake.eclipse__',
-)
-
 java_library(
   name = 'classpath',
   deps = LIBS + PGMLIBS + [
@@ -35,37 +13,3 @@
     '//lib/prolog:compiler_lib',
   ] + scan_plugins(),
 )
-
-genrule(
-  name = '_project',
-  cmd = '$(exe :gen_project)',
-  deps = [':gen_project'],
-  out = '__fake.project__',
-)
-
-genrule(
-  name = '_classpath',
-  cmd = '$(exe :gen_classpath)',
-  deps = [
-    ':classpath',
-    ':gen_classpath',
-  ],
-  out = '__fake.classpath__',
-)
-
-genrule(
-  name = '_classpath_nocompile',
-  cmd = '$(exe :gen_classpath)',
-  deps = [':gen_classpath'],
-  out = '__fake.classpath__',
-)
-
-python_binary(
-  name = 'gen_classpath',
-  main = 'gen_classpath.py',
-)
-
-python_binary(
-  name = 'gen_project',
-  main = 'gen_project.py',
-)
diff --git a/tools/eclipse/gen_classpath.py b/tools/eclipse/gen_classpath.py
deleted file mode 100755
index 241a876..0000000
--- a/tools/eclipse/gen_classpath.py
+++ /dev/null
@@ -1,118 +0,0 @@
-#!/usr/bin/python
-# 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.
-#
-# TODO(sop): Remove hack after Buck supports Eclipse
-
-from os import path
-import re
-from subprocess import Popen, PIPE
-from sys import argv
-from xml.dom import minidom
-
-ROOT = path.abspath(__file__)
-for _ in range(0, 3):
-  ROOT = path.dirname(ROOT)
-
-MAIN = ['//tools/eclipse:classpath']
-GWT = ['//gerrit-gwtui:ui_module']
-JRE = '/'.join([
-  'org.eclipse.jdt.launching.JRE_CONTAINER',
-  'org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType',
-  'JavaSE-1.6',
-])
-
-def query_classpath(targets):
-  deps = []
-  p = Popen(['buck', 'audit', 'classpath'] + targets, stdout = PIPE)
-  for line in p.stdout:
-    deps.append(line.strip())
-  s = p.wait()
-  if s != 0:
-    exit(s)
-  return deps
-
-def make_classpath():
-  impl = minidom.getDOMImplementation()
-  return impl.createDocument(None, 'classpath', None)
-
-doc = make_classpath()
-src = set()
-lib = set()
-gwt_src = set()
-gwt_lib = set()
-
-def classpathentry(kind, path, src = None):
-  e = doc.createElement('classpathentry')
-  e.setAttribute('kind', kind)
-  e.setAttribute('path', path)
-  if src:
-    e.setAttribute('sourcepath', src)
-  doc.documentElement.appendChild(e)
-
-java_library = re.compile(r'[^/]+/gen/(.*)/lib__[^/]+__output/[^/]+[.]jar$')
-for p in query_classpath(MAIN):
-  if p.endswith('-src.jar'):
-    # gwt_module() depends on -src.jar for Java to JavaScript compiles.
-    gwt_lib.add(p)
-    continue
-
-  if p.startswith('buck-out/gen/lib/gwt/'):
-    # gwt_module() depends on huge shaded GWT JARs that import
-    # incorrect versions of classes for Gerrit. Collect into
-    # a private grouping for later use.
-    gwt_lib.add(p)
-    continue
-
-  m = java_library.match(p)
-  if m:
-    src.add(m.group(1))
-  else:
-    lib.add(p)
-
-for p in query_classpath(GWT):
-  m = java_library.match(p)
-  if m:
-    gwt_src.add(m.group(1))
-
-for s in sorted(src):
-  p = path.join(s, 'java')
-  if path.exists(p):
-    classpathentry('src', p)
-    continue
-
-  for env in ['main', 'test']:
-    for type in ['java', 'resources']:
-      p = path.join(s, 'src', env, type)
-      if path.exists(p):
-        classpathentry('src', p)
-
-for libs in [lib, gwt_lib]:
-  for j in sorted(libs):
-    s = None
-    if j.endswith('.jar'):
-      s = j[:-4] + '-src.jar'
-      if not path.exists(s):
-        s = None
-    classpathentry('lib', j, s)
-
-for s in sorted(gwt_src):
-  classpathentry('lib', path.join(ROOT, s, 'src', 'main', 'java'))
-
-classpathentry('con', JRE)
-classpathentry('output', 'buck-out/classes')
-
-p = path.join(ROOT, '.classpath')
-with open(p, 'w') as fd:
-  doc.writexml(fd, addindent = '  ', newl = '\n', encoding='UTF-8')
diff --git a/tools/eclipse/gen_project.py b/tools/eclipse/gen_project.py
deleted file mode 100755
index d42d43c..0000000
--- a/tools/eclipse/gen_project.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/python
-# 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.
-#
-# TODO(sop): Remove hack after Buck supports Eclipse
-
-from __future__ import print_function
-
-from os import path
-from sys import argv
-
-ROOT = path.abspath(__file__)
-for _ in range(0, 3):
-  ROOT = path.dirname(ROOT)
-
-p = path.join(ROOT, '.project')
-with open(p, 'w') as fd:
-  print("""\
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
-  <name>gerrit</name>
-  <buildSpec>
-    <buildCommand>
-      <name>org.eclipse.jdt.core.javabuilder</name>
-    </buildCommand>
-  </buildSpec>
-  <natures>
-    <nature>org.eclipse.jdt.core.javanature</nature>
-  </natures>
-</projectDescription>\
-""", file=fd)
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
new file mode 100755
index 0000000..884d0ff
--- /dev/null
+++ b/tools/eclipse/project.py
@@ -0,0 +1,157 @@
+#!/usr/bin/python
+# 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.
+#
+# TODO(sop): Remove hack after Buck supports Eclipse
+
+from __future__ import print_function
+from optparse import OptionParser
+from os import path
+from subprocess import Popen, PIPE, CalledProcessError, check_call
+from xml.dom import minidom
+import re
+
+MAIN = ['//tools/eclipse:classpath']
+GWT = ['//gerrit-gwtui:ui_module']
+JRE = '/'.join([
+  'org.eclipse.jdt.launching.JRE_CONTAINER',
+  'org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType',
+  'JavaSE-1.6',
+])
+
+ROOT = path.abspath(__file__)
+for _ in range(0, 3):
+  ROOT = path.dirname(ROOT)
+
+opts = OptionParser()
+opts.add_option('--src', action='store_true')
+args, _ = opts.parse_args()
+
+def gen_project():
+  p = path.join(ROOT, '.project')
+  with open(p, 'w') as fd:
+    print("""\
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+  <name>gerrit</name>
+  <buildSpec>
+    <buildCommand>
+      <name>org.eclipse.jdt.core.javabuilder</name>
+    </buildCommand>
+  </buildSpec>
+  <natures>
+    <nature>org.eclipse.jdt.core.javanature</nature>
+  </natures>
+</projectDescription>\
+""", file=fd)
+
+def gen_classpath():
+  def query_classpath(targets):
+    deps = []
+    p = Popen(['buck', 'audit', 'classpath'] + targets, stdout=PIPE)
+    for line in p.stdout:
+      deps.append(line.strip())
+    s = p.wait()
+    if s != 0:
+      exit(s)
+    return deps
+
+  def make_classpath():
+    impl = minidom.getDOMImplementation()
+    return impl.createDocument(None, 'classpath', None)
+
+  def classpathentry(kind, path, src=None):
+    e = doc.createElement('classpathentry')
+    e.setAttribute('kind', kind)
+    e.setAttribute('path', path)
+    if src:
+      e.setAttribute('sourcepath', src)
+    doc.documentElement.appendChild(e)
+
+  doc = make_classpath()
+  src = set()
+  lib = set()
+  gwt_src = set()
+  gwt_lib = set()
+
+  java_library = re.compile(r'[^/]+/gen/(.*)/lib__[^/]+__output/[^/]+[.]jar$')
+  for p in query_classpath(MAIN):
+    if p.endswith('-src.jar'):
+      # gwt_module() depends on -src.jar for Java to JavaScript compiles.
+      gwt_lib.add(p)
+      continue
+
+    if p.startswith('buck-out/gen/lib/gwt/'):
+      # gwt_module() depends on huge shaded GWT JARs that import
+      # incorrect versions of classes for Gerrit. Collect into
+      # a private grouping for later use.
+      gwt_lib.add(p)
+      continue
+
+    m = java_library.match(p)
+    if m:
+      src.add(m.group(1))
+    else:
+      lib.add(p)
+
+  for p in query_classpath(GWT):
+    m = java_library.match(p)
+    if m:
+      gwt_src.add(m.group(1))
+
+  for s in sorted(src):
+    p = path.join(s, 'java')
+    if path.exists(p):
+      classpathentry('src', p)
+      continue
+
+    for env in ['main', 'test']:
+      for type in ['java', 'resources']:
+        p = path.join(s, 'src', env, type)
+        if path.exists(p):
+          classpathentry('src', p)
+
+  for libs in [lib, gwt_lib]:
+    for j in sorted(libs):
+      s = None
+      if j.endswith('.jar'):
+        s = j[:-4] + '-src.jar'
+        if not path.exists(s):
+          s = None
+      classpathentry('lib', j, s)
+
+  for s in sorted(gwt_src):
+    classpathentry('lib', path.join(ROOT, s, 'src', 'main', 'java'))
+
+  classpathentry('con', JRE)
+  classpathentry('output', 'buck-out/classes')
+
+  p = path.join(ROOT, '.classpath')
+  with open(p, 'w') as fd:
+    doc.writexml(fd, addindent='  ', newl='\n', encoding='UTF-8')
+
+if args.src:
+  try:
+    check_call([path.join(ROOT, 'tools', 'download_all.py'), '--src'])
+  except CalledProcessError as err:
+    exit(1)
+
+gen_project()
+gen_classpath()
+
+try:
+  targets = ['//tools:buck.properties'] + MAIN + GWT
+  check_call(['buck', 'build'] + targets)
+except CalledProcessError as err:
+  exit(1)
diff --git a/tools/maven/fake_pom.xml b/tools/maven/fake_pom.xml
new file mode 100644
index 0000000..d066a4a
--- /dev/null
+++ b/tools/maven/fake_pom.xml
@@ -0,0 +1,6 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>com.google.gerrit</groupId>
+  <artifactId>Gerrit-Code-Review-Maven</artifactId>
+  <version>1</version>
+</project>
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index dc098a3..fefc5b3 100644
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -15,12 +15,14 @@
 
 from __future__ import print_function
 from optparse import OptionParser
+from os import path
 from sys import stderr
 from util import check_output
 
 opts = OptionParser()
 opts.add_option('--repository', help='maven repository id')
 opts.add_option('--url', help='maven repository url')
+opts.add_option('-o')
 opts.add_option('-a', help='action (valid actions are: install,deploy)')
 opts.add_option('-v', help='gerrit version')
 opts.add_option('-s', action='append', help='triplet of artifactId:type:path')
@@ -35,11 +37,13 @@
   '-Dversion=%s' % args.v,
 ]
 
+self = path.dirname(path.abspath(__file__))
+mvn = ['mvn', '--file', path.join(self, 'fake_pom.xml')]
+
 if 'install' == args.a:
-  cmd = ['mvn', 'install:install-file'] + common
+  cmd = mvn + ['install:install-file'] + common
 elif 'deploy' == args.a:
-  cmd = [
-    'mvn',
+  cmd = mvn + [
     'deploy:deploy-file',
     '-DrepositoryId=%s' % args.repository,
     '-Durl=%s' % args.url,
@@ -59,3 +63,10 @@
   except Exception as e:
     print('%s command failed: %s' % (args.a, e), file=stderr)
     exit(1)
+
+with open(args.o, 'w') as fd:
+  if args.repository:
+    print('Repository: %s' % args.repository, file=fd)
+  if args.url:
+    print('URL: %s' % args.url, file=fd)
+  print('Version: %s' % args.v, file=fd)
diff --git a/tools/maven/package.defs b/tools/maven/package.defs
index b19701f..7306031 100644
--- a/tools/maven/package.defs
+++ b/tools/maven/package.defs
@@ -18,7 +18,7 @@
     url = None,
     jar = {},
     src = {}):
-  cmd = ['$(exe //tools/maven:mvn)', '-v', version]
+  cmd = ['$(exe //tools/maven:mvn)', '-v', version, '-o', '$OUT']
   dep = []
 
   for type,d in [('jar', jar), ('java-source', src)]:
@@ -30,7 +30,7 @@
     name = 'install',
     cmd = ' '.join(cmd + ['-a', 'install']),
     deps = dep + ['//tools/maven:mvn'],
-    out = '__fake.install__',
+    out = 'install.info',
   )
 
   if repository and url:
@@ -41,5 +41,5 @@
         '--repository', repository,
         '--url', url]),
       deps = dep + ['//tools/maven:mvn'],
-      out = '__fake.deploy__',
+      out = 'deploy.info',
     )